@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.
- package/LICENSE +202 -0
- package/README.md +52 -0
- package/assets/chatroom.svg +15 -0
- package/chatroom.config.example.json +24 -0
- package/dist/app/application.d.ts +20 -0
- package/dist/app/application.js +75 -0
- package/dist/app/event-bus.d.ts +25 -0
- package/dist/app/event-bus.js +11 -0
- package/dist/app/external-access-registry.d.ts +17 -0
- package/dist/app/external-access-registry.js +41 -0
- package/dist/app/lifecycle.d.ts +14 -0
- package/dist/app/lifecycle.js +130 -0
- package/dist/auth/auth-service.d.ts +88 -0
- package/dist/auth/auth-service.js +221 -0
- package/dist/auth/ingress-policy.d.ts +23 -0
- package/dist/auth/ingress-policy.js +61 -0
- package/dist/auth/passkey-service.d.ts +39 -0
- package/dist/auth/passkey-service.js +145 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +10 -0
- package/dist/cli/run-cli.d.ts +1 -0
- package/dist/cli/run-cli.js +41 -0
- package/dist/config/load-config.d.ts +7 -0
- package/dist/config/load-config.js +197 -0
- package/dist/config/platform-paths.d.ts +7 -0
- package/dist/config/platform-paths.js +34 -0
- package/dist/config/types.d.ts +24 -0
- package/dist/config/types.js +1 -0
- package/dist/core/auth/passkey-repository.d.ts +18 -0
- package/dist/core/auth/passkey-repository.js +1 -0
- package/dist/core/auth/repository.d.ts +36 -0
- package/dist/core/auth/repository.js +1 -0
- package/dist/core/auth/session-repository.d.ts +6 -0
- package/dist/core/auth/session-repository.js +1 -0
- package/dist/core/errors/chatroom-error.d.ts +9 -0
- package/dist/core/errors/chatroom-error.js +31 -0
- package/dist/core/logging/types.d.ts +31 -0
- package/dist/core/logging/types.js +1 -0
- package/dist/core/operations/bounded-value.d.ts +6 -0
- package/dist/core/operations/bounded-value.js +57 -0
- package/dist/core/operations/redactor.d.ts +4 -0
- package/dist/core/operations/redactor.js +69 -0
- package/dist/core/operations/repository.d.ts +18 -0
- package/dist/core/operations/repository.js +1 -0
- package/dist/core/operations/types.d.ts +28 -0
- package/dist/core/operations/types.js +8 -0
- package/dist/core/runtime/child-environment.d.ts +1 -0
- package/dist/core/runtime/child-environment.js +39 -0
- package/dist/core/runtime/command-runner.d.ts +16 -0
- package/dist/core/runtime/command-runner.js +48 -0
- package/dist/core/runtime/identity.d.ts +2 -0
- package/dist/core/runtime/identity.js +9 -0
- package/dist/infrastructure/database/app-database.d.ts +6 -0
- package/dist/infrastructure/database/app-database.js +22 -0
- package/dist/infrastructure/database/mcp-tool-settings-repository.d.ts +8 -0
- package/dist/infrastructure/database/mcp-tool-settings-repository.js +22 -0
- package/dist/infrastructure/database/oauth-repository.d.ts +20 -0
- package/dist/infrastructure/database/oauth-repository.js +140 -0
- package/dist/infrastructure/database/operation-repository.d.ts +16 -0
- package/dist/infrastructure/database/operation-repository.js +120 -0
- package/dist/infrastructure/database/passkey-repository.d.ts +11 -0
- package/dist/infrastructure/database/passkey-repository.js +57 -0
- package/dist/infrastructure/database/schema.d.ts +1 -0
- package/dist/infrastructure/database/schema.js +90 -0
- package/dist/infrastructure/database/web-session-repository.d.ts +10 -0
- package/dist/infrastructure/database/web-session-repository.js +26 -0
- package/dist/infrastructure/http/http-server.d.ts +28 -0
- package/dist/infrastructure/http/http-server.js +146 -0
- package/dist/infrastructure/http/ingress-middleware.d.ts +6 -0
- package/dist/infrastructure/http/ingress-middleware.js +58 -0
- package/dist/infrastructure/logging/file-log-store.d.ts +9 -0
- package/dist/infrastructure/logging/file-log-store.js +173 -0
- package/dist/infrastructure/logging/system-log.d.ts +22 -0
- package/dist/infrastructure/logging/system-log.js +117 -0
- package/dist/mcp/server/create-mcp-server.d.ts +3 -0
- package/dist/mcp/server/create-mcp-server.js +16 -0
- package/dist/mcp/server/plugin-mcp-registrar.d.ts +31 -0
- package/dist/mcp/server/plugin-mcp-registrar.js +70 -0
- package/dist/mcp/server/request-context.d.ts +3 -0
- package/dist/mcp/server/request-context.js +8 -0
- package/dist/mcp/server/tool-control.d.ts +31 -0
- package/dist/mcp/server/tool-control.js +76 -0
- package/dist/mcp/server/tool-support.d.ts +44 -0
- package/dist/mcp/server/tool-support.js +72 -0
- package/dist/native/linux/chatroom-computer-helper +0 -0
- package/dist/native/macos/ChatRoomComputerHelper.app/Contents/Info.plist +14 -0
- package/dist/native/macos/ChatRoomComputerHelper.app/Contents/MacOS/chatroom-computer-helper +0 -0
- package/dist/native/macos/ChatRoomComputerHelper.app/Contents/_CodeSignature/CodeResources +115 -0
- package/dist/native/windows/chatroom-computer-helper.exe +0 -0
- package/dist/operations/operation-log.d.ts +24 -0
- package/dist/operations/operation-log.js +98 -0
- package/dist/plugins/cloud/api-client.d.ts +50 -0
- package/dist/plugins/cloud/api-client.js +112 -0
- package/dist/plugins/cloud/controller.d.ts +58 -0
- package/dist/plugins/cloud/controller.js +425 -0
- package/dist/plugins/cloud/device-proof.d.ts +9 -0
- package/dist/plugins/cloud/device-proof.js +17 -0
- package/dist/plugins/cloud/plugin.d.ts +4 -0
- package/dist/plugins/cloud/plugin.js +17 -0
- package/dist/plugins/cloud/state-store.d.ts +7 -0
- package/dist/plugins/cloud/state-store.js +79 -0
- package/dist/plugins/cloud/tunnel-client.d.ts +84 -0
- package/dist/plugins/cloud/tunnel-client.js +427 -0
- package/dist/plugins/cloud/types.d.ts +72 -0
- package/dist/plugins/cloud/types.js +60 -0
- package/dist/plugins/computer/audit.d.ts +34 -0
- package/dist/plugins/computer/audit.js +42 -0
- package/dist/plugins/computer/computer-native-backend.d.ts +11 -0
- package/dist/plugins/computer/computer-native-backend.js +58 -0
- package/dist/plugins/computer/computer-native-host.d.ts +30 -0
- package/dist/plugins/computer/computer-native-host.js +411 -0
- package/dist/plugins/computer/computer-protocol.d.ts +22 -0
- package/dist/plugins/computer/computer-protocol.js +74 -0
- package/dist/plugins/computer/computer-schemas.d.ts +285 -0
- package/dist/plugins/computer/computer-schemas.js +152 -0
- package/dist/plugins/computer/computer-service.d.ts +29 -0
- package/dist/plugins/computer/computer-service.js +117 -0
- package/dist/plugins/computer/computer-settings-repository.d.ts +8 -0
- package/dist/plugins/computer/computer-settings-repository.js +41 -0
- package/dist/plugins/computer/mcp.d.ts +3 -0
- package/dist/plugins/computer/mcp.js +126 -0
- package/dist/plugins/computer/plugin.d.ts +4 -0
- package/dist/plugins/computer/plugin.js +29 -0
- package/dist/plugins/computer/types.d.ts +174 -0
- package/dist/plugins/computer/types.js +1 -0
- package/dist/plugins/git/git-service.d.ts +30 -0
- package/dist/plugins/git/git-service.js +428 -0
- package/dist/plugins/git/plugin.d.ts +4 -0
- package/dist/plugins/git/plugin.js +12 -0
- package/dist/plugins/git/types.d.ts +32 -0
- package/dist/plugins/git/types.js +1 -0
- package/dist/plugins/plugin-manager.d.ts +11 -0
- package/dist/plugins/plugin-manager.js +63 -0
- package/dist/plugins/process/backend.d.ts +18 -0
- package/dist/plugins/process/backend.js +1 -0
- package/dist/plugins/process/head-tail-buffer.d.ts +17 -0
- package/dist/plugins/process/head-tail-buffer.js +49 -0
- package/dist/plugins/process/infrastructure/pipe-process-backend.d.ts +8 -0
- package/dist/plugins/process/infrastructure/pipe-process-backend.js +71 -0
- package/dist/plugins/process/infrastructure/pty-process-backend.d.ts +8 -0
- package/dist/plugins/process/infrastructure/pty-process-backend.js +51 -0
- package/dist/plugins/process/mcp.d.ts +3 -0
- package/dist/plugins/process/mcp.js +58 -0
- package/dist/plugins/process/plugin.d.ts +4 -0
- package/dist/plugins/process/plugin.js +25 -0
- package/dist/plugins/process/process-supervisor.d.ts +39 -0
- package/dist/plugins/process/process-supervisor.js +244 -0
- package/dist/plugins/process/types.d.ts +30 -0
- package/dist/plugins/process/types.js +1 -0
- package/dist/plugins/types.d.ts +35 -0
- package/dist/plugins/types.js +17 -0
- package/dist/plugins/web/api-types.d.ts +56 -0
- package/dist/plugins/web/api-types.js +1 -0
- package/dist/plugins/web/http/api-router.d.ts +13 -0
- package/dist/plugins/web/http/api-router.js +47 -0
- package/dist/plugins/web/http/auth-api-router.d.ts +8 -0
- package/dist/plugins/web/http/auth-api-router.js +123 -0
- package/dist/plugins/web/http/cloud-api-router.d.ts +4 -0
- package/dist/plugins/web/http/cloud-api-router.js +29 -0
- package/dist/plugins/web/http/computer-api-router.d.ts +5 -0
- package/dist/plugins/web/http/computer-api-router.js +69 -0
- package/dist/plugins/web/http/git-api-router.d.ts +3 -0
- package/dist/plugins/web/http/git-api-router.js +85 -0
- package/dist/plugins/web/http/logs-api-router.d.ts +3 -0
- package/dist/plugins/web/http/logs-api-router.js +49 -0
- package/dist/plugins/web/http/process-api-router.d.ts +3 -0
- package/dist/plugins/web/http/process-api-router.js +16 -0
- package/dist/plugins/web/http/runtime-api-router.d.ts +10 -0
- package/dist/plugins/web/http/runtime-api-router.js +34 -0
- package/dist/plugins/web/http/workspace-api-router.d.ts +3 -0
- package/dist/plugins/web/http/workspace-api-router.js +91 -0
- package/dist/plugins/web/plugin.d.ts +8 -0
- package/dist/plugins/web/plugin.js +17 -0
- package/dist/plugins/web/runtime.d.ts +18 -0
- package/dist/plugins/web/runtime.js +31 -0
- package/dist/plugins/workspace/mcp.d.ts +3 -0
- package/dist/plugins/workspace/mcp.js +24 -0
- package/dist/plugins/workspace/metadata.d.ts +6 -0
- package/dist/plugins/workspace/metadata.js +111 -0
- package/dist/plugins/workspace/plugin.d.ts +4 -0
- package/dist/plugins/workspace/plugin.js +22 -0
- package/dist/plugins/workspace/types.d.ts +23 -0
- package/dist/plugins/workspace/types.js +1 -0
- package/dist/plugins/workspace/workspace-fs.d.ts +28 -0
- package/dist/plugins/workspace/workspace-fs.js +200 -0
- package/dist/plugins/workspace/workspace-service.d.ts +15 -0
- package/dist/plugins/workspace/workspace-service.js +140 -0
- package/dist/presentation/http/http-utils.d.ts +9 -0
- package/dist/presentation/http/http-utils.js +89 -0
- package/dist/presentation/http/oauth-router.d.ts +5 -0
- package/dist/presentation/http/oauth-router.js +172 -0
- package/dist/web/assets/CloudView-CHjVbp-5.css +1 -0
- package/dist/web/assets/CloudView-CvX0dkgu.js +1 -0
- package/dist/web/assets/CodeViewer-CH1DEJge.css +1 -0
- package/dist/web/assets/CodeViewer.vue_vue_type_script_setup_true_lang-B5TxGkUW.js +3 -0
- package/dist/web/assets/ComputerView-91Z9Cefw.css +1 -0
- package/dist/web/assets/ComputerView-OOyEqWml.js +1 -0
- package/dist/web/assets/McpToolsView-BTnolyS6.css +1 -0
- package/dist/web/assets/McpToolsView-DjXWTKrZ.js +1 -0
- package/dist/web/assets/OperationsView-DqTY51Cr.js +4 -0
- package/dist/web/assets/ProcessesView-C13BykIa.js +4 -0
- package/dist/web/assets/StateChip.vue_vue_type_script_setup_true_lang-DyOhJmR6.js +1 -0
- package/dist/web/assets/SystemLogsView-Ci0u4BTb.js +1 -0
- package/dist/web/assets/VSelect-CAxEESw-.css +1 -0
- package/dist/web/assets/VSelect-DrRpKyYI.js +1 -0
- package/dist/web/assets/VSpacer-C3wAY3F0.js +1 -0
- package/dist/web/assets/VSpacer-DfbUir7X.css +1 -0
- package/dist/web/assets/VSwitch-Cos8PNSP.css +1 -0
- package/dist/web/assets/VSwitch-CxusR4iO.js +1 -0
- package/dist/web/assets/VTabs-DSWgbwhc.css +1 -0
- package/dist/web/assets/VTabs-qBcuuXh3.js +1 -0
- package/dist/web/assets/WorkspacesView-BzQ0aH74.css +1 -0
- package/dist/web/assets/WorkspacesView-CijIvfsp.js +2 -0
- package/dist/web/assets/_plugin-vue_export-helper-DlAUqK2U.js +1 -0
- package/dist/web/assets/chatroom-B5p-5n3y.svg +15 -0
- package/dist/web/assets/index-BDkarAkX.js +2 -0
- package/dist/web/assets/index-BegKQ3Yd.js +19 -0
- package/dist/web/assets/index-CkjqXRSj.css +1 -0
- package/dist/web/assets/syntax-highlight-B5ArnyrC.js +3 -0
- package/dist/web/assets/useOperationLabels-BDjzGDkT.js +1 -0
- package/dist/web/assets/utils-rtPwWNel.js +1 -0
- package/dist/web/index.html +19 -0
- package/package.json +125 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { ChatRoomError } from "#core/errors/chatroom-error";
|
|
7
|
+
import { platformPaths } from "./platform-paths.js";
|
|
8
|
+
function rawConfigSchema() {
|
|
9
|
+
const defaults = defaultConfig();
|
|
10
|
+
return z
|
|
11
|
+
.object({
|
|
12
|
+
allowedRoots: z
|
|
13
|
+
.array(z.string().min(1))
|
|
14
|
+
.min(1)
|
|
15
|
+
.default(defaults.allowedRoots),
|
|
16
|
+
dataDir: z.string().min(1).default(defaults.dataDir),
|
|
17
|
+
databasePath: z.string().min(1),
|
|
18
|
+
server: z
|
|
19
|
+
.object({
|
|
20
|
+
host: z.string().min(1).default(defaults.server.host),
|
|
21
|
+
port: z
|
|
22
|
+
.number()
|
|
23
|
+
.int()
|
|
24
|
+
.min(1)
|
|
25
|
+
.max(65535)
|
|
26
|
+
.default(defaults.server.port),
|
|
27
|
+
})
|
|
28
|
+
.strict()
|
|
29
|
+
.default(defaults.server),
|
|
30
|
+
auth: z
|
|
31
|
+
.object({
|
|
32
|
+
localWebAuth: z.boolean().default(defaults.auth.localWebAuth),
|
|
33
|
+
ownerToken: z
|
|
34
|
+
.string()
|
|
35
|
+
.min(1)
|
|
36
|
+
.nullable()
|
|
37
|
+
.default(defaults.auth.ownerToken),
|
|
38
|
+
mcpPublicBaseUrl: z
|
|
39
|
+
.string()
|
|
40
|
+
.url()
|
|
41
|
+
.nullable()
|
|
42
|
+
.default(defaults.auth.mcpPublicBaseUrl),
|
|
43
|
+
webPublicBaseUrl: z
|
|
44
|
+
.string()
|
|
45
|
+
.url()
|
|
46
|
+
.nullable()
|
|
47
|
+
.default(defaults.auth.webPublicBaseUrl),
|
|
48
|
+
allowedRedirectHosts: z
|
|
49
|
+
.array(z.string().min(1))
|
|
50
|
+
.default(defaults.auth.allowedRedirectHosts),
|
|
51
|
+
})
|
|
52
|
+
.strict()
|
|
53
|
+
.default(defaults.auth),
|
|
54
|
+
operations: z
|
|
55
|
+
.object({
|
|
56
|
+
maxPayloadBytes: z
|
|
57
|
+
.number()
|
|
58
|
+
.int()
|
|
59
|
+
.min(4096)
|
|
60
|
+
.max(16 * 1024 * 1024)
|
|
61
|
+
.default(defaults.operations.maxPayloadBytes),
|
|
62
|
+
})
|
|
63
|
+
.strict()
|
|
64
|
+
.default(defaults.operations),
|
|
65
|
+
process: z
|
|
66
|
+
.object({
|
|
67
|
+
maxOutputBytes: z
|
|
68
|
+
.number()
|
|
69
|
+
.int()
|
|
70
|
+
.min(4096)
|
|
71
|
+
.max(64 * 1024 * 1024)
|
|
72
|
+
.default(defaults.process.maxOutputBytes),
|
|
73
|
+
defaultTimeoutMs: z
|
|
74
|
+
.number()
|
|
75
|
+
.int()
|
|
76
|
+
.min(1000)
|
|
77
|
+
.max(24 * 60 * 60 * 1000)
|
|
78
|
+
.default(defaults.process.defaultTimeoutMs),
|
|
79
|
+
maxCompletedProcesses: z
|
|
80
|
+
.number()
|
|
81
|
+
.int()
|
|
82
|
+
.min(0)
|
|
83
|
+
.max(10_000)
|
|
84
|
+
.default(defaults.process.maxCompletedProcesses),
|
|
85
|
+
})
|
|
86
|
+
.strict()
|
|
87
|
+
.default(defaults.process),
|
|
88
|
+
})
|
|
89
|
+
.strict();
|
|
90
|
+
}
|
|
91
|
+
function defaultConfigPath() {
|
|
92
|
+
return process.env.CHATROOM_CONFIG ?? platformPaths().configFile;
|
|
93
|
+
}
|
|
94
|
+
export function defaultConfig() {
|
|
95
|
+
const paths = platformPaths();
|
|
96
|
+
return {
|
|
97
|
+
allowedRoots: [path.join(os.homedir(), "Projects")],
|
|
98
|
+
dataDir: paths.dataDir,
|
|
99
|
+
databasePath: paths.databaseFile,
|
|
100
|
+
server: { host: "127.0.0.1", port: 8765 },
|
|
101
|
+
auth: {
|
|
102
|
+
localWebAuth: false,
|
|
103
|
+
ownerToken: null,
|
|
104
|
+
mcpPublicBaseUrl: null,
|
|
105
|
+
webPublicBaseUrl: null,
|
|
106
|
+
allowedRedirectHosts: ["chatgpt.com", "localhost", "127.0.0.1"],
|
|
107
|
+
},
|
|
108
|
+
operations: { maxPayloadBytes: 512 * 1024 },
|
|
109
|
+
process: {
|
|
110
|
+
maxOutputBytes: 512 * 1024,
|
|
111
|
+
defaultTimeoutMs: 30 * 60 * 1000,
|
|
112
|
+
maxCompletedProcesses: 200,
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
export async function loadConfig(configPath = defaultConfigPath()) {
|
|
117
|
+
let parsed;
|
|
118
|
+
try {
|
|
119
|
+
parsed = JSON.parse(await readFile(configPath, "utf8"));
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
if (error.code === "ENOENT")
|
|
123
|
+
throw new ChatRoomError("NOT_FOUND", `ChatRoom is not initialized: ${configPath}`);
|
|
124
|
+
throw new ChatRoomError("INVALID_INPUT", `Cannot read config: ${configPath}`, undefined, {
|
|
125
|
+
cause: error,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
const config = validateConfig(parsed);
|
|
129
|
+
validateRuntimeSecurity(config);
|
|
130
|
+
return config;
|
|
131
|
+
}
|
|
132
|
+
export async function initializeConfig() {
|
|
133
|
+
const configPath = defaultConfigPath();
|
|
134
|
+
try {
|
|
135
|
+
await readFile(configPath, "utf8");
|
|
136
|
+
throw new ChatRoomError("CONFLICT", `Config already exists: ${configPath}`);
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
if (error instanceof ChatRoomError)
|
|
140
|
+
throw error;
|
|
141
|
+
if (error.code !== "ENOENT")
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
const config = defaultConfig();
|
|
145
|
+
await mkdir(config.allowedRoots[0], { recursive: true, mode: 0o700 });
|
|
146
|
+
config.auth.ownerToken = randomBytes(32).toString("base64url");
|
|
147
|
+
await mkdir(path.dirname(configPath), { recursive: true, mode: 0o700 });
|
|
148
|
+
await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, {
|
|
149
|
+
mode: 0o600,
|
|
150
|
+
});
|
|
151
|
+
await chmod(configPath, 0o600).catch(() => undefined);
|
|
152
|
+
return { config, configPath };
|
|
153
|
+
}
|
|
154
|
+
function validateConfig(value) {
|
|
155
|
+
const result = rawConfigSchema().safeParse(value);
|
|
156
|
+
if (!result.success)
|
|
157
|
+
throw new ChatRoomError("INVALID_INPUT", "Invalid ChatRoom configuration", {
|
|
158
|
+
issues: result.error.issues.map((issue) => ({
|
|
159
|
+
path: issue.path.join("."),
|
|
160
|
+
message: issue.message,
|
|
161
|
+
})),
|
|
162
|
+
});
|
|
163
|
+
const dataDir = path.resolve(result.data.dataDir);
|
|
164
|
+
return {
|
|
165
|
+
...result.data,
|
|
166
|
+
allowedRoots: result.data.allowedRoots.map((root) => path.resolve(root)),
|
|
167
|
+
dataDir,
|
|
168
|
+
databasePath: path.resolve(result.data.databasePath),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function validateRuntimeSecurity(config) {
|
|
172
|
+
validatePublicBaseUrl(config.auth.mcpPublicBaseUrl, "auth.mcpPublicBaseUrl");
|
|
173
|
+
validatePublicBaseUrl(config.auth.webPublicBaseUrl, "auth.webPublicBaseUrl");
|
|
174
|
+
const loopback = config.server.host === "127.0.0.1" ||
|
|
175
|
+
config.server.host === "::1" ||
|
|
176
|
+
config.server.host === "localhost";
|
|
177
|
+
if (!loopback && !config.auth.localWebAuth)
|
|
178
|
+
throw new ChatRoomError("FORBIDDEN", "auth.localWebAuth must be enabled when ChatRoom binds beyond loopback");
|
|
179
|
+
const authenticationUsed = config.auth.localWebAuth ||
|
|
180
|
+
Boolean(config.auth.mcpPublicBaseUrl) ||
|
|
181
|
+
Boolean(config.auth.webPublicBaseUrl);
|
|
182
|
+
if (authenticationUsed && !config.auth.ownerToken)
|
|
183
|
+
throw new ChatRoomError("INVALID_INPUT", "auth.ownerToken is required when any authenticated ingress is configured");
|
|
184
|
+
}
|
|
185
|
+
function validatePublicBaseUrl(value, name) {
|
|
186
|
+
if (!value)
|
|
187
|
+
return;
|
|
188
|
+
const url = new URL(value);
|
|
189
|
+
if (url.protocol !== "http:" && url.protocol !== "https:")
|
|
190
|
+
throw new ChatRoomError("INVALID_INPUT", name + " must use HTTP or HTTPS");
|
|
191
|
+
if (url.username ||
|
|
192
|
+
url.password ||
|
|
193
|
+
url.pathname !== "/" ||
|
|
194
|
+
url.search ||
|
|
195
|
+
url.hash)
|
|
196
|
+
throw new ChatRoomError("INVALID_INPUT", name + " must be an origin without credentials, path, query, or fragment");
|
|
197
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export function platformPaths(platform = process.platform, env = process.env, home = os.homedir()) {
|
|
4
|
+
if (platform === "win32") {
|
|
5
|
+
const join = path.win32.join;
|
|
6
|
+
const roaming = env.APPDATA ?? join(home, "AppData", "Roaming");
|
|
7
|
+
const local = env.LOCALAPPDATA ?? join(home, "AppData", "Local");
|
|
8
|
+
return {
|
|
9
|
+
configFile: join(roaming, "ChatRoom", "config.json"),
|
|
10
|
+
dataDir: join(local, "ChatRoom", "Data"),
|
|
11
|
+
databaseFile: join(local, "ChatRoom", "State", "chatroom.sqlite"),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
const join = path.posix.join;
|
|
15
|
+
if (platform === "darwin") {
|
|
16
|
+
const applicationSupport = join(home, "Library", "Application Support", "ChatRoom");
|
|
17
|
+
return {
|
|
18
|
+
configFile: join(applicationSupport, "config.json"),
|
|
19
|
+
dataDir: join(applicationSupport, "Data"),
|
|
20
|
+
databaseFile: join(applicationSupport, "State", "chatroom.sqlite"),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
const configHome = absoluteXdg(env.XDG_CONFIG_HOME) ?? join(home, ".config");
|
|
24
|
+
const dataHome = absoluteXdg(env.XDG_DATA_HOME) ?? join(home, ".local", "share");
|
|
25
|
+
const stateHome = absoluteXdg(env.XDG_STATE_HOME) ?? join(home, ".local", "state");
|
|
26
|
+
return {
|
|
27
|
+
configFile: join(configHome, "chatroom", "config.json"),
|
|
28
|
+
dataDir: join(dataHome, "chatroom"),
|
|
29
|
+
databaseFile: join(stateHome, "chatroom", "chatroom.sqlite"),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function absoluteXdg(value) {
|
|
33
|
+
return value && path.posix.isAbsolute(value) ? value : null;
|
|
34
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface ChatRoomConfig {
|
|
2
|
+
allowedRoots: string[];
|
|
3
|
+
dataDir: string;
|
|
4
|
+
databasePath: string;
|
|
5
|
+
server: {
|
|
6
|
+
host: string;
|
|
7
|
+
port: number;
|
|
8
|
+
};
|
|
9
|
+
auth: {
|
|
10
|
+
localWebAuth: boolean;
|
|
11
|
+
ownerToken: string | null;
|
|
12
|
+
mcpPublicBaseUrl: string | null;
|
|
13
|
+
webPublicBaseUrl: string | null;
|
|
14
|
+
allowedRedirectHosts: string[];
|
|
15
|
+
};
|
|
16
|
+
operations: {
|
|
17
|
+
maxPayloadBytes: number;
|
|
18
|
+
};
|
|
19
|
+
process: {
|
|
20
|
+
maxOutputBytes: number;
|
|
21
|
+
defaultTimeoutMs: number;
|
|
22
|
+
maxCompletedProcesses: number;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface PasskeyRecord {
|
|
2
|
+
credentialId: string;
|
|
3
|
+
publicKey: Uint8Array;
|
|
4
|
+
counter: number;
|
|
5
|
+
transports: string[];
|
|
6
|
+
deviceType: "singleDevice" | "multiDevice";
|
|
7
|
+
backedUp: boolean;
|
|
8
|
+
name: string;
|
|
9
|
+
createdAt: string;
|
|
10
|
+
lastUsedAt: string;
|
|
11
|
+
}
|
|
12
|
+
export interface PasskeyRepository {
|
|
13
|
+
list(): PasskeyRecord[];
|
|
14
|
+
get(credentialId: string): PasskeyRecord | null;
|
|
15
|
+
upsert(record: PasskeyRecord): void;
|
|
16
|
+
remove(credentialId: string): void;
|
|
17
|
+
updateCounter(credentialId: string, counter: number): void;
|
|
18
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface OAuthClientRecord {
|
|
2
|
+
clientId: string;
|
|
3
|
+
name: string;
|
|
4
|
+
redirectUris: string[];
|
|
5
|
+
createdAt: string;
|
|
6
|
+
}
|
|
7
|
+
export interface OAuthCodeRecord {
|
|
8
|
+
codeHash: string;
|
|
9
|
+
clientId: string;
|
|
10
|
+
redirectUri: string;
|
|
11
|
+
codeChallenge: string;
|
|
12
|
+
scope: string;
|
|
13
|
+
expiresAt: string;
|
|
14
|
+
usedAt: string | null;
|
|
15
|
+
}
|
|
16
|
+
export interface OAuthTokenRecord {
|
|
17
|
+
clientId: string;
|
|
18
|
+
scope: string;
|
|
19
|
+
expiresAt: string;
|
|
20
|
+
}
|
|
21
|
+
export interface OAuthStateRepository {
|
|
22
|
+
createClient(client: OAuthClientRecord): void;
|
|
23
|
+
getClient(clientId: string): OAuthClientRecord | null;
|
|
24
|
+
createCode(code: OAuthCodeRecord): void;
|
|
25
|
+
getCode(codeHash: string): OAuthCodeRecord | null;
|
|
26
|
+
consumeCode(codeHash: string, now: string): OAuthCodeRecord | null;
|
|
27
|
+
createToken(tokenHash: string, clientId: string, scope: string, expiresAt: string): void;
|
|
28
|
+
createRefreshToken(tokenHash: string, clientId: string, scope: string, expiresAt: string): void;
|
|
29
|
+
consumeRefreshToken(tokenHash: string, clientId?: string): {
|
|
30
|
+
clientId: string;
|
|
31
|
+
scope: string;
|
|
32
|
+
} | null;
|
|
33
|
+
tokenInfo(tokenHash: string): OAuthTokenRecord | null;
|
|
34
|
+
revokeToken(tokenHash: string): void;
|
|
35
|
+
prune(now: string): number;
|
|
36
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
declare const ERROR_CODES: readonly ["INVALID_INPUT", "NOT_FOUND", "FORBIDDEN", "CONFLICT", "UNSUPPORTED", "PROCESS_FAILED", "INTERNAL"];
|
|
2
|
+
export type ErrorCode = (typeof ERROR_CODES)[number];
|
|
3
|
+
export declare class ChatRoomError extends Error {
|
|
4
|
+
readonly code: ErrorCode;
|
|
5
|
+
readonly details?: unknown;
|
|
6
|
+
constructor(code: ErrorCode, message: string, details?: unknown, options?: ErrorOptions);
|
|
7
|
+
}
|
|
8
|
+
export declare function asChatRoomError(error: unknown): ChatRoomError;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const ERROR_CODES = [
|
|
2
|
+
"INVALID_INPUT",
|
|
3
|
+
"NOT_FOUND",
|
|
4
|
+
"FORBIDDEN",
|
|
5
|
+
"CONFLICT",
|
|
6
|
+
"UNSUPPORTED",
|
|
7
|
+
"PROCESS_FAILED",
|
|
8
|
+
"INTERNAL",
|
|
9
|
+
];
|
|
10
|
+
export class ChatRoomError extends Error {
|
|
11
|
+
code;
|
|
12
|
+
details;
|
|
13
|
+
constructor(code, message, details, options) {
|
|
14
|
+
super(message, options);
|
|
15
|
+
this.name = "ChatRoomError";
|
|
16
|
+
this.code = code;
|
|
17
|
+
this.details = details;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function asChatRoomError(error) {
|
|
21
|
+
if (error instanceof ChatRoomError)
|
|
22
|
+
return error;
|
|
23
|
+
if (error instanceof Error) {
|
|
24
|
+
return new ChatRoomError("INTERNAL", error.message, undefined, {
|
|
25
|
+
cause: error,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
return new ChatRoomError("INTERNAL", "Unknown internal error", {
|
|
29
|
+
value: String(error),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type LogLevel = "debug" | "info" | "warn" | "error";
|
|
2
|
+
export interface LogRecord {
|
|
3
|
+
id: string;
|
|
4
|
+
timestamp: string;
|
|
5
|
+
level: LogLevel;
|
|
6
|
+
module: string;
|
|
7
|
+
event: string;
|
|
8
|
+
message: string;
|
|
9
|
+
data?: Record<string, unknown>;
|
|
10
|
+
}
|
|
11
|
+
export interface LogQuery {
|
|
12
|
+
limit: number;
|
|
13
|
+
cursor?: string;
|
|
14
|
+
levels?: ReadonlySet<LogLevel>;
|
|
15
|
+
modules?: ReadonlySet<string>;
|
|
16
|
+
}
|
|
17
|
+
export interface LogPage {
|
|
18
|
+
items: LogRecord[];
|
|
19
|
+
nextCursor: string | null;
|
|
20
|
+
}
|
|
21
|
+
export interface LogWriter {
|
|
22
|
+
debug(module: string, event: string, message: string, data?: Record<string, unknown>): void;
|
|
23
|
+
info(module: string, event: string, message: string, data?: Record<string, unknown>): void;
|
|
24
|
+
warn(module: string, event: string, message: string, data?: Record<string, unknown>): void;
|
|
25
|
+
error(module: string, event: string, message: string, data?: Record<string, unknown>): void;
|
|
26
|
+
}
|
|
27
|
+
export interface LogService extends LogWriter {
|
|
28
|
+
list(query: LogQuery): Promise<LogPage>;
|
|
29
|
+
subscribe(listener: (record: LogRecord) => void): () => void;
|
|
30
|
+
flush(): Promise<void>;
|
|
31
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export function boundJson(value, maxBytes) {
|
|
2
|
+
if (value === undefined)
|
|
3
|
+
return { value: null, truncated: false };
|
|
4
|
+
const json = safeStringify(value);
|
|
5
|
+
const size = Buffer.byteLength(json);
|
|
6
|
+
if (size <= maxBytes)
|
|
7
|
+
return { value: JSON.parse(json), truncated: false };
|
|
8
|
+
const encoded = Buffer.from(json);
|
|
9
|
+
const marker = "\n…[truncated]…\n";
|
|
10
|
+
let low = 0;
|
|
11
|
+
let high = encoded.length;
|
|
12
|
+
let best = truncatedValue(encoded, marker, size, 0);
|
|
13
|
+
while (low <= high) {
|
|
14
|
+
const retainedBytes = Math.floor((low + high) / 2);
|
|
15
|
+
const candidate = truncatedValue(encoded, marker, size, retainedBytes);
|
|
16
|
+
if (Buffer.byteLength(JSON.stringify(candidate)) <= maxBytes) {
|
|
17
|
+
best = candidate;
|
|
18
|
+
low = retainedBytes + 1;
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
high = retainedBytes - 1;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return { value: best, truncated: true };
|
|
25
|
+
}
|
|
26
|
+
function truncatedValue(encoded, marker, originalBytes, retainedBytes) {
|
|
27
|
+
const headBytes = Math.floor(retainedBytes / 2);
|
|
28
|
+
const tailBytes = retainedBytes - headBytes;
|
|
29
|
+
const head = encoded.subarray(0, headBytes).toString("utf8");
|
|
30
|
+
const tail = encoded
|
|
31
|
+
.subarray(Math.max(0, encoded.length - tailBytes))
|
|
32
|
+
.toString("utf8");
|
|
33
|
+
return {
|
|
34
|
+
truncatedJson: `${head}${marker}${tail}`,
|
|
35
|
+
originalBytes,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function safeStringify(value) {
|
|
39
|
+
const seen = new WeakSet();
|
|
40
|
+
return (JSON.stringify(value, (_key, current) => {
|
|
41
|
+
if (typeof current === "bigint")
|
|
42
|
+
return current.toString();
|
|
43
|
+
if (current instanceof Error) {
|
|
44
|
+
return {
|
|
45
|
+
name: current.name,
|
|
46
|
+
message: current.message,
|
|
47
|
+
stack: current.stack,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (current && typeof current === "object") {
|
|
51
|
+
if (seen.has(current))
|
|
52
|
+
return "[circular]";
|
|
53
|
+
seen.add(current);
|
|
54
|
+
}
|
|
55
|
+
return current;
|
|
56
|
+
}) ?? "null");
|
|
57
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const SECRET_KEY = new RegExp("(^|[_-])" +
|
|
2
|
+
"(authorization|cookie|password|passwd|token|access[_-]?token|" +
|
|
3
|
+
"refresh[_-]?token|api[_-]?key|apikey|secret|private[_-]?key|" +
|
|
4
|
+
"client[_-]?secret)" +
|
|
5
|
+
"($|[_-])", "i");
|
|
6
|
+
const BEARER = /\bBearer\s+[A-Za-z0-9._~+\/-]+=*/gi;
|
|
7
|
+
const PEM = /-----BEGIN [^-]*PRIVATE KEY-----[\s\S]*?-----END [^-]*PRIVATE KEY-----/g;
|
|
8
|
+
const INLINE_SECRET = /\b(password|passwd|token|api[_-]?key|secret|client[_-]?secret)\s*[:=]\s*([^\s,;]+)/gi;
|
|
9
|
+
const CLI_SECRET = /(--(?:password|passwd|token|api-key|api_key|secret|client-secret))\s+([^\s]+)/gi;
|
|
10
|
+
export class SecretRedactor {
|
|
11
|
+
redact(value) {
|
|
12
|
+
return this.walk(value, new WeakSet());
|
|
13
|
+
}
|
|
14
|
+
walk(value, seen) {
|
|
15
|
+
if (typeof value === "string") {
|
|
16
|
+
return value
|
|
17
|
+
.replace(BEARER, "Bearer [redacted]")
|
|
18
|
+
.replace(PEM, "[redacted]")
|
|
19
|
+
.replace(INLINE_SECRET, "$1=[redacted]")
|
|
20
|
+
.replace(CLI_SECRET, "$1 [redacted]");
|
|
21
|
+
}
|
|
22
|
+
if (value === null || typeof value !== "object")
|
|
23
|
+
return value;
|
|
24
|
+
if (seen.has(value))
|
|
25
|
+
return "[circular]";
|
|
26
|
+
seen.add(value);
|
|
27
|
+
try {
|
|
28
|
+
if (Array.isArray(value))
|
|
29
|
+
return value.map((item) => this.walk(item, seen));
|
|
30
|
+
const result = {};
|
|
31
|
+
for (const [key, item] of Object.entries(value)) {
|
|
32
|
+
result[key] = SECRET_KEY.test(key)
|
|
33
|
+
? "[redacted]"
|
|
34
|
+
: key === "args" && Array.isArray(item)
|
|
35
|
+
? redactArguments(item, (entry) => this.walk(entry, seen))
|
|
36
|
+
: this.walk(item, seen);
|
|
37
|
+
}
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
seen.delete(value);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function redactArguments(args, redactValue) {
|
|
46
|
+
const output = [];
|
|
47
|
+
let redactNext = false;
|
|
48
|
+
for (const arg of args) {
|
|
49
|
+
if (redactNext) {
|
|
50
|
+
output.push("[redacted]");
|
|
51
|
+
redactNext = false;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (typeof arg === "string") {
|
|
55
|
+
const equals = /^(--(?:password|passwd|token|api-key|api_key|secret|client-secret))=(.*)$/i.exec(arg);
|
|
56
|
+
if (equals) {
|
|
57
|
+
output.push(`${equals[1]}=[redacted]`);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (/^--(?:password|passwd|token|api-key|api_key|secret|client-secret)$/i.test(arg)) {
|
|
61
|
+
output.push(arg);
|
|
62
|
+
redactNext = true;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
output.push(redactValue(arg));
|
|
67
|
+
}
|
|
68
|
+
return output;
|
|
69
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Operation, OperationStatus } from "./types.js";
|
|
2
|
+
export interface OperationQuery {
|
|
3
|
+
limit?: number;
|
|
4
|
+
offset?: number;
|
|
5
|
+
pluginId?: string;
|
|
6
|
+
status?: OperationStatus | string;
|
|
7
|
+
}
|
|
8
|
+
export interface OperationRepository {
|
|
9
|
+
insert(operation: Operation): void;
|
|
10
|
+
update(operation: Operation): void;
|
|
11
|
+
get(operationId: string): Operation | null;
|
|
12
|
+
list(query?: OperationQuery): Operation[];
|
|
13
|
+
clearHistory(): {
|
|
14
|
+
deleted: number;
|
|
15
|
+
preserved: number;
|
|
16
|
+
};
|
|
17
|
+
reconcileRunning(finishedAt: string): number;
|
|
18
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
declare const OPERATION_STATUSES: readonly ["running", "success", "error", "cancelled"];
|
|
2
|
+
export type OperationStatus = (typeof OPERATION_STATUSES)[number];
|
|
3
|
+
declare const OPERATION_SOURCES: readonly ["mcp", "gui", "system", "cli"];
|
|
4
|
+
export type OperationSource = (typeof OPERATION_SOURCES)[number];
|
|
5
|
+
export interface Operation {
|
|
6
|
+
operationId: string;
|
|
7
|
+
pluginId: string;
|
|
8
|
+
source: OperationSource;
|
|
9
|
+
action: string;
|
|
10
|
+
status: OperationStatus;
|
|
11
|
+
processId: string | null;
|
|
12
|
+
input: unknown;
|
|
13
|
+
output: unknown;
|
|
14
|
+
error: unknown;
|
|
15
|
+
inputTruncated: boolean;
|
|
16
|
+
outputTruncated: boolean;
|
|
17
|
+
startedAt: string;
|
|
18
|
+
finishedAt: string | null;
|
|
19
|
+
durationMs: number | null;
|
|
20
|
+
}
|
|
21
|
+
export interface OperationStart {
|
|
22
|
+
pluginId: string;
|
|
23
|
+
source: OperationSource;
|
|
24
|
+
action: string;
|
|
25
|
+
input?: unknown;
|
|
26
|
+
processId?: string | null;
|
|
27
|
+
}
|
|
28
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function childEnvironment(overrides: Record<string, string> | undefined): NodeJS.ProcessEnv;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const CHILD_ENV_KEYS = new Set([
|
|
2
|
+
"PATH",
|
|
3
|
+
"HOME",
|
|
4
|
+
"USER",
|
|
5
|
+
"LOGNAME",
|
|
6
|
+
"SHELL",
|
|
7
|
+
"LANG",
|
|
8
|
+
"LANGUAGE",
|
|
9
|
+
"TERM",
|
|
10
|
+
"COLORTERM",
|
|
11
|
+
"TMPDIR",
|
|
12
|
+
"TMP",
|
|
13
|
+
"TEMP",
|
|
14
|
+
"XDG_CONFIG_HOME",
|
|
15
|
+
"XDG_CACHE_HOME",
|
|
16
|
+
"XDG_DATA_HOME",
|
|
17
|
+
"XDG_STATE_HOME",
|
|
18
|
+
"XDG_RUNTIME_DIR",
|
|
19
|
+
"NVM_DIR",
|
|
20
|
+
"NVM_BIN",
|
|
21
|
+
"SystemRoot",
|
|
22
|
+
"WINDIR",
|
|
23
|
+
"ComSpec",
|
|
24
|
+
"PATHEXT",
|
|
25
|
+
"USERPROFILE",
|
|
26
|
+
"LOCALAPPDATA",
|
|
27
|
+
"APPDATA",
|
|
28
|
+
]);
|
|
29
|
+
export function childEnvironment(overrides) {
|
|
30
|
+
const env = {};
|
|
31
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
32
|
+
if (value !== undefined &&
|
|
33
|
+
(CHILD_ENV_KEYS.has(key) || key.startsWith("LC_")))
|
|
34
|
+
env[key] = value;
|
|
35
|
+
}
|
|
36
|
+
for (const [key, value] of Object.entries(overrides ?? {}))
|
|
37
|
+
env[key] = value;
|
|
38
|
+
return env;
|
|
39
|
+
}
|