@chatroomcp/chatroom 0.1.3

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 (159) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +50 -0
  3. package/assets/chatroom.svg +15 -0
  4. package/chatroom.config.example.json +24 -0
  5. package/dist/app/application.d.ts +18 -0
  6. package/dist/app/application.js +63 -0
  7. package/dist/app/event-bus.d.ts +23 -0
  8. package/dist/app/event-bus.js +12 -0
  9. package/dist/app/external-access-registry.d.ts +18 -0
  10. package/dist/app/external-access-registry.js +48 -0
  11. package/dist/app/lifecycle.d.ts +12 -0
  12. package/dist/app/lifecycle.js +41 -0
  13. package/dist/auth/auth-service.d.ts +88 -0
  14. package/dist/auth/auth-service.js +222 -0
  15. package/dist/auth/ingress-policy.d.ts +21 -0
  16. package/dist/auth/ingress-policy.js +57 -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 +42 -0
  23. package/dist/config/load-config.d.ts +7 -0
  24. package/dist/config/load-config.js +182 -0
  25. package/dist/config/platform-paths.d.ts +6 -0
  26. package/dist/config/platform-paths.js +34 -0
  27. package/dist/config/types.d.ts +25 -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 +59 -0
  32. package/dist/core/auth/repository.js +7 -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 +23 -0
  36. package/dist/core/errors/chatroom-error.js +46 -0
  37. package/dist/core/operations/bounded-value.d.ts +6 -0
  38. package/dist/core/operations/bounded-value.js +41 -0
  39. package/dist/core/operations/redactor.d.ts +4 -0
  40. package/dist/core/operations/redactor.js +65 -0
  41. package/dist/core/operations/repository.d.ts +19 -0
  42. package/dist/core/operations/repository.js +1 -0
  43. package/dist/core/operations/types.d.ts +31 -0
  44. package/dist/core/operations/types.js +9 -0
  45. package/dist/core/runtime/child-environment.d.ts +1 -0
  46. package/dist/core/runtime/child-environment.js +39 -0
  47. package/dist/core/runtime/command-runner.d.ts +15 -0
  48. package/dist/core/runtime/command-runner.js +39 -0
  49. package/dist/core/runtime/identity.d.ts +2 -0
  50. package/dist/core/runtime/identity.js +9 -0
  51. package/dist/infrastructure/database/app-database.d.ts +6 -0
  52. package/dist/infrastructure/database/app-database.js +104 -0
  53. package/dist/infrastructure/database/oauth-repository.d.ts +20 -0
  54. package/dist/infrastructure/database/oauth-repository.js +140 -0
  55. package/dist/infrastructure/database/operation-repository.d.ts +16 -0
  56. package/dist/infrastructure/database/operation-repository.js +129 -0
  57. package/dist/infrastructure/database/passkey-repository.d.ts +11 -0
  58. package/dist/infrastructure/database/passkey-repository.js +57 -0
  59. package/dist/infrastructure/database/web-session-repository.d.ts +10 -0
  60. package/dist/infrastructure/database/web-session-repository.js +26 -0
  61. package/dist/infrastructure/http/http-server.d.ts +26 -0
  62. package/dist/infrastructure/http/http-server.js +166 -0
  63. package/dist/mcp/server/create-mcp-server.d.ts +3 -0
  64. package/dist/mcp/server/create-mcp-server.js +16 -0
  65. package/dist/mcp/server/tool-support.d.ts +105 -0
  66. package/dist/mcp/server/tool-support.js +124 -0
  67. package/dist/operations/operation-log.d.ts +23 -0
  68. package/dist/operations/operation-log.js +103 -0
  69. package/dist/plugins/cloud/api-client.d.ts +56 -0
  70. package/dist/plugins/cloud/api-client.js +122 -0
  71. package/dist/plugins/cloud/controller.d.ts +48 -0
  72. package/dist/plugins/cloud/controller.js +282 -0
  73. package/dist/plugins/cloud/device-proof.d.ts +9 -0
  74. package/dist/plugins/cloud/device-proof.js +17 -0
  75. package/dist/plugins/cloud/plugin.d.ts +4 -0
  76. package/dist/plugins/cloud/plugin.js +17 -0
  77. package/dist/plugins/cloud/state-store.d.ts +7 -0
  78. package/dist/plugins/cloud/state-store.js +74 -0
  79. package/dist/plugins/cloud/tunnel-client.d.ts +36 -0
  80. package/dist/plugins/cloud/tunnel-client.js +293 -0
  81. package/dist/plugins/cloud/types.d.ts +70 -0
  82. package/dist/plugins/cloud/types.js +32 -0
  83. package/dist/plugins/plugin-manager.d.ts +11 -0
  84. package/dist/plugins/plugin-manager.js +24 -0
  85. package/dist/plugins/process/backend.d.ts +19 -0
  86. package/dist/plugins/process/backend.js +1 -0
  87. package/dist/plugins/process/head-tail-buffer.d.ts +18 -0
  88. package/dist/plugins/process/head-tail-buffer.js +49 -0
  89. package/dist/plugins/process/infrastructure/pipe-process-backend.d.ts +8 -0
  90. package/dist/plugins/process/infrastructure/pipe-process-backend.js +61 -0
  91. package/dist/plugins/process/infrastructure/pty-process-backend.d.ts +8 -0
  92. package/dist/plugins/process/infrastructure/pty-process-backend.js +50 -0
  93. package/dist/plugins/process/mcp.d.ts +4 -0
  94. package/dist/plugins/process/mcp.js +69 -0
  95. package/dist/plugins/process/plugin.d.ts +4 -0
  96. package/dist/plugins/process/plugin.js +28 -0
  97. package/dist/plugins/process/process-supervisor.d.ts +36 -0
  98. package/dist/plugins/process/process-supervisor.js +232 -0
  99. package/dist/plugins/process/types.d.ts +34 -0
  100. package/dist/plugins/process/types.js +1 -0
  101. package/dist/plugins/types.d.ts +32 -0
  102. package/dist/plugins/types.js +17 -0
  103. package/dist/plugins/web/api-types.d.ts +10 -0
  104. package/dist/plugins/web/api-types.js +1 -0
  105. package/dist/plugins/web/http/api-router.d.ts +13 -0
  106. package/dist/plugins/web/http/api-router.js +146 -0
  107. package/dist/plugins/web/http/cloud-api-router.d.ts +4 -0
  108. package/dist/plugins/web/http/cloud-api-router.js +41 -0
  109. package/dist/plugins/web/http/process-api-router.d.ts +3 -0
  110. package/dist/plugins/web/http/process-api-router.js +16 -0
  111. package/dist/plugins/web/http/workspace-api-router.d.ts +3 -0
  112. package/dist/plugins/web/http/workspace-api-router.js +82 -0
  113. package/dist/plugins/web/plugin.d.ts +8 -0
  114. package/dist/plugins/web/plugin.js +17 -0
  115. package/dist/plugins/web/runtime.d.ts +36 -0
  116. package/dist/plugins/web/runtime.js +66 -0
  117. package/dist/plugins/workspace/domain/filesystem.d.ts +25 -0
  118. package/dist/plugins/workspace/domain/filesystem.js +1 -0
  119. package/dist/plugins/workspace/domain/git.d.ts +7 -0
  120. package/dist/plugins/workspace/domain/git.js +1 -0
  121. package/dist/plugins/workspace/domain/repository.d.ts +16 -0
  122. package/dist/plugins/workspace/domain/repository.js +1 -0
  123. package/dist/plugins/workspace/domain/review.d.ts +28 -0
  124. package/dist/plugins/workspace/domain/review.js +1 -0
  125. package/dist/plugins/workspace/domain/workspace.d.ts +23 -0
  126. package/dist/plugins/workspace/domain/workspace.js +1 -0
  127. package/dist/plugins/workspace/git/git-command-runner.d.ts +6 -0
  128. package/dist/plugins/workspace/git/git-command-runner.js +15 -0
  129. package/dist/plugins/workspace/git/git-service.d.ts +14 -0
  130. package/dist/plugins/workspace/git/git-service.js +107 -0
  131. package/dist/plugins/workspace/git/git-snapshot-service.d.ts +33 -0
  132. package/dist/plugins/workspace/git/git-snapshot-service.js +352 -0
  133. package/dist/plugins/workspace/infrastructure/workspace-fs.d.ts +38 -0
  134. package/dist/plugins/workspace/infrastructure/workspace-fs.js +340 -0
  135. package/dist/plugins/workspace/infrastructure/workspace-repository.d.ts +12 -0
  136. package/dist/plugins/workspace/infrastructure/workspace-repository.js +51 -0
  137. package/dist/plugins/workspace/mcp.d.ts +4 -0
  138. package/dist/plugins/workspace/mcp.js +168 -0
  139. package/dist/plugins/workspace/plugin.d.ts +10 -0
  140. package/dist/plugins/workspace/plugin.js +37 -0
  141. package/dist/plugins/workspace/workspace-helpers.d.ts +9 -0
  142. package/dist/plugins/workspace/workspace-helpers.js +67 -0
  143. package/dist/plugins/workspace/workspace-service.d.ts +46 -0
  144. package/dist/plugins/workspace/workspace-service.js +339 -0
  145. package/dist/plugins/workspace/worktree-review-service.d.ts +16 -0
  146. package/dist/plugins/workspace/worktree-review-service.js +51 -0
  147. package/dist/presentation/http/http-utils.d.ts +7 -0
  148. package/dist/presentation/http/http-utils.js +47 -0
  149. package/dist/presentation/http/oauth-router.d.ts +5 -0
  150. package/dist/presentation/http/oauth-router.js +163 -0
  151. package/dist/web/assets/chatroom-B5p-5n3y.svg +15 -0
  152. package/dist/web/assets/index-BF_mWl1D.js +26 -0
  153. package/dist/web/assets/index-JPZut9tc.css +1 -0
  154. package/dist/web/assets/materialdesignicons-webfont-B7mPwVP_.ttf +0 -0
  155. package/dist/web/assets/materialdesignicons-webfont-CSr8KVlo.eot +0 -0
  156. package/dist/web/assets/materialdesignicons-webfont-Dp5v-WZN.woff2 +0 -0
  157. package/dist/web/assets/materialdesignicons-webfont-PXm3-2wK.woff +0 -0
  158. package/dist/web/index.html +19 -0
  159. package/package.json +77 -0
@@ -0,0 +1,182 @@
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.js";
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).optional(),
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 ?? path.join(dataDir, "chatroom.sqlite")),
169
+ };
170
+ }
171
+ function validateRuntimeSecurity(config) {
172
+ const loopback = config.server.host === "127.0.0.1" ||
173
+ config.server.host === "::1" ||
174
+ config.server.host === "localhost";
175
+ if (!loopback && !config.auth.localWebAuth)
176
+ throw new ChatRoomError("FORBIDDEN", "auth.localWebAuth must be enabled when ChatRoom binds beyond loopback");
177
+ const authenticationUsed = config.auth.localWebAuth ||
178
+ Boolean(config.auth.mcpPublicBaseUrl) ||
179
+ Boolean(config.auth.webPublicBaseUrl);
180
+ if (authenticationUsed && !config.auth.ownerToken)
181
+ throw new ChatRoomError("INVALID_INPUT", "auth.ownerToken is required when any authenticated ingress is configured");
182
+ }
@@ -0,0 +1,6 @@
1
+ export interface PlatformPaths {
2
+ configFile: string;
3
+ dataDir: string;
4
+ databaseFile: string;
5
+ }
6
+ export declare function platformPaths(platform?: NodeJS.Platform, env?: NodeJS.ProcessEnv, home?: string): PlatformPaths;
@@ -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,25 @@
1
+ /** Runtime configuration after defaults and path normalization have been resolved. */
2
+ export interface ChatRoomConfig {
3
+ allowedRoots: string[];
4
+ dataDir: string;
5
+ databasePath: string;
6
+ server: {
7
+ host: string;
8
+ port: number;
9
+ };
10
+ auth: {
11
+ localWebAuth: boolean;
12
+ ownerToken: string | null;
13
+ mcpPublicBaseUrl: string | null;
14
+ webPublicBaseUrl: string | null;
15
+ allowedRedirectHosts: string[];
16
+ };
17
+ operations: {
18
+ maxPayloadBytes: number;
19
+ };
20
+ process: {
21
+ maxOutputBytes: number;
22
+ defaultTimeoutMs: number;
23
+ maxCompletedProcesses: number;
24
+ };
25
+ }
@@ -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,59 @@
1
+ /**
2
+ * Persistence contracts for OAuth clients, authorization codes, and access tokens.
3
+ *
4
+ * Raw authorization codes and bearer tokens must never cross this boundary. AuthService hashes
5
+ * those credentials first and repositories store only the resulting digest values.
6
+ */
7
+ /** Dynamically registered OAuth public client. */
8
+ export interface OAuthClientRecord {
9
+ clientId: string;
10
+ name: string;
11
+ /** Exact redirect URI allowlist registered for the client. */
12
+ redirectUris: string[];
13
+ createdAt: string;
14
+ }
15
+ /** Hashed Authorization Code + PKCE state. */
16
+ export interface OAuthCodeRecord {
17
+ /** SHA-256 digest of the raw one-time authorization code. */
18
+ codeHash: string;
19
+ clientId: string;
20
+ redirectUri: string;
21
+ /** S256 challenge supplied during authorization. */
22
+ codeChallenge: string;
23
+ /** Granted scopes bound to this authorization code. */
24
+ scope: string;
25
+ expiresAt: string;
26
+ /** Consumption timestamp, or null before the code is atomically used. */
27
+ usedAt: string | null;
28
+ }
29
+ /** Minimal active-token metadata returned after a hashed token lookup. */
30
+ export interface OAuthTokenRecord {
31
+ clientId: string;
32
+ scope: string;
33
+ expiresAt: string;
34
+ }
35
+ /** Storage port used by AuthService for the complete OAuth state machine. */
36
+ export interface OAuthStateRepository {
37
+ createClient(client: OAuthClientRecord): void;
38
+ getClient(clientId: string): OAuthClientRecord | null;
39
+ createCode(code: OAuthCodeRecord): void;
40
+ /** Read an unconsumed candidate without mutating it so PKCE can be validated first. */
41
+ getCode(codeHash: string): OAuthCodeRecord | null;
42
+ /** Atomically consume a valid code; null means it was missing, expired by policy, or already used. */
43
+ consumeCode(codeHash: string, now: string): OAuthCodeRecord | null;
44
+ /** Persist only a token hash together with scope and expiry metadata. */
45
+ createToken(tokenHash: string, clientId: string, scope: string, expiresAt: string): void;
46
+ /** Persist a hashed refresh token separately from access tokens. */
47
+ createRefreshToken(tokenHash: string, clientId: string, scope: string, expiresAt: string): void;
48
+ /** Atomically rotate an active refresh token. */
49
+ consumeRefreshToken(tokenHash: string, clientId?: string): {
50
+ clientId: string;
51
+ scope: string;
52
+ } | null;
53
+ /** Resolve active, non-revoked token metadata from a token hash. */
54
+ tokenInfo(tokenHash: string): OAuthTokenRecord | null;
55
+ /** Mark a token hash revoked without requiring the raw bearer token to be stored. */
56
+ revokeToken(tokenHash: string): void;
57
+ /** Remove expired and terminal OAuth state that no longer participates in the protocol. */
58
+ prune(now: string): number;
59
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Persistence contracts for OAuth clients, authorization codes, and access tokens.
3
+ *
4
+ * Raw authorization codes and bearer tokens must never cross this boundary. AuthService hashes
5
+ * those credentials first and repositories store only the resulting digest values.
6
+ */
7
+ export {};
@@ -0,0 +1,6 @@
1
+ export interface WebSessionRepository {
2
+ create(tokenHash: string, expiresAt: string): void;
3
+ valid(tokenHash: string, now: string): boolean;
4
+ revoke(tokenHash: string): void;
5
+ prune(now: string): number;
6
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Typed error vocabulary exposed across ChatRoom application boundaries.
3
+ *
4
+ * Presentation adapters translate these stable codes into protocol responses. Domain/application
5
+ * code should prefer ChatRoomError over parsing message text or leaking implementation-specific
6
+ * filesystem, SQLite, process, or SDK errors to external callers.
7
+ */
8
+ export declare const ERROR_CODES: readonly ["INVALID_INPUT", "NOT_FOUND", "FORBIDDEN", "CONFLICT", "UNSUPPORTED", "PROCESS_FAILED", "INTERNAL"];
9
+ export type ErrorCode = (typeof ERROR_CODES)[number];
10
+ /** Error carrying a stable machine-readable code plus optional structured diagnostic details. */
11
+ export declare class ChatRoomError extends Error {
12
+ readonly code: ErrorCode;
13
+ readonly details?: unknown;
14
+ constructor(code: ErrorCode, message: string, details?: unknown, options?: ErrorOptions);
15
+ }
16
+ /**
17
+ * Normalize an arbitrary thrown value at an application or protocol boundary.
18
+ *
19
+ * Existing ChatRoomError instances retain their public code. Native Error values become INTERNAL
20
+ * while preserving the original error as `cause`; non-Error throws are converted as well so every
21
+ * external path observes the same error vocabulary.
22
+ */
23
+ export declare function asChatRoomError(error: unknown): ChatRoomError;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Typed error vocabulary exposed across ChatRoom application boundaries.
3
+ *
4
+ * Presentation adapters translate these stable codes into protocol responses. Domain/application
5
+ * code should prefer ChatRoomError over parsing message text or leaking implementation-specific
6
+ * filesystem, SQLite, process, or SDK errors to external callers.
7
+ */
8
+ export const ERROR_CODES = [
9
+ "INVALID_INPUT",
10
+ "NOT_FOUND",
11
+ "FORBIDDEN",
12
+ "CONFLICT",
13
+ "UNSUPPORTED",
14
+ "PROCESS_FAILED",
15
+ "INTERNAL",
16
+ ];
17
+ /** Error carrying a stable machine-readable code plus optional structured diagnostic details. */
18
+ export class ChatRoomError extends Error {
19
+ code;
20
+ details;
21
+ constructor(code, message, details, options) {
22
+ super(message, options);
23
+ this.name = "ChatRoomError";
24
+ this.code = code;
25
+ this.details = details;
26
+ }
27
+ }
28
+ /**
29
+ * Normalize an arbitrary thrown value at an application or protocol boundary.
30
+ *
31
+ * Existing ChatRoomError instances retain their public code. Native Error values become INTERNAL
32
+ * while preserving the original error as `cause`; non-Error throws are converted as well so every
33
+ * external path observes the same error vocabulary.
34
+ */
35
+ export function asChatRoomError(error) {
36
+ if (error instanceof ChatRoomError)
37
+ return error;
38
+ if (error instanceof Error) {
39
+ return new ChatRoomError("INTERNAL", error.message, undefined, {
40
+ cause: error,
41
+ });
42
+ }
43
+ return new ChatRoomError("INTERNAL", "Unknown internal error", {
44
+ value: String(error),
45
+ });
46
+ }
@@ -0,0 +1,6 @@
1
+ /** Utilities for bounding JSON-like operation payloads before persistence. */
2
+ export interface BoundedValue<T = unknown> {
3
+ value: T;
4
+ truncated: boolean;
5
+ }
6
+ export declare function boundJson(value: unknown, maxBytes: number): BoundedValue;
@@ -0,0 +1,41 @@
1
+ export function boundJson(value, maxBytes) {
2
+ // Preserve both the beginning and end of oversized strings because failures commonly appear at the tail.
3
+ if (value === undefined)
4
+ return { value: null, truncated: false };
5
+ const json = safeStringify(value);
6
+ const size = Buffer.byteLength(json);
7
+ if (size <= maxBytes)
8
+ return { value: JSON.parse(json), truncated: false };
9
+ const marker = "\n…[truncated]…\n";
10
+ const budget = Math.max(0, maxBytes - Buffer.byteLength(marker));
11
+ const headBudget = Math.floor(budget / 2);
12
+ const tailBudget = budget - headBudget;
13
+ const encoded = Buffer.from(json);
14
+ const head = encoded.subarray(0, headBudget).toString("utf8");
15
+ const tail = encoded.subarray(encoded.length - tailBudget).toString("utf8");
16
+ const text = `${head}${marker}${tail}`;
17
+ return {
18
+ value: { truncatedJson: text, originalBytes: size },
19
+ truncated: true,
20
+ };
21
+ }
22
+ function safeStringify(value) {
23
+ const seen = new WeakSet();
24
+ return (JSON.stringify(value, (_key, current) => {
25
+ if (typeof current === "bigint")
26
+ return current.toString();
27
+ if (current instanceof Error) {
28
+ return {
29
+ name: current.name,
30
+ message: current.message,
31
+ stack: current.stack,
32
+ };
33
+ }
34
+ if (current && typeof current === "object") {
35
+ if (seen.has(current))
36
+ return "[circular]";
37
+ seen.add(current);
38
+ }
39
+ return current;
40
+ }) ?? "null");
41
+ }
@@ -0,0 +1,4 @@
1
+ export declare class SecretRedactor {
2
+ redact(value: unknown): unknown;
3
+ private walk;
4
+ }
@@ -0,0 +1,65 @@
1
+ /** Redacts common credential shapes before values cross the durable operation-log boundary. */
2
+ const SECRET_KEY = new RegExp("(^|[_-])" +
3
+ "(authorization|cookie|password|passwd|token|access[_-]?token|" +
4
+ "refresh[_-]?token|api[_-]?key|apikey|secret|private[_-]?key|" +
5
+ "client[_-]?secret)" +
6
+ "($|[_-])", "i");
7
+ const BEARER = /\bBearer\s+[A-Za-z0-9._~+\/-]+=*/gi;
8
+ const PEM = /-----BEGIN [^-]*PRIVATE KEY-----[\s\S]*?-----END [^-]*PRIVATE KEY-----/g;
9
+ const INLINE_SECRET = /\b(password|passwd|token|api[_-]?key|secret|client[_-]?secret)\s*[:=]\s*([^\s,;]+)/gi;
10
+ const CLI_SECRET = /(--(?:password|passwd|token|api-key|api_key|secret|client-secret))\s+([^\s]+)/gi;
11
+ export class SecretRedactor {
12
+ redact(value) {
13
+ return this.walk(value, new WeakSet());
14
+ }
15
+ walk(value, seen) {
16
+ if (typeof value === "string") {
17
+ return value
18
+ .replace(BEARER, "Bearer [redacted]")
19
+ .replace(PEM, "[redacted]")
20
+ .replace(INLINE_SECRET, "$1=[redacted]")
21
+ .replace(CLI_SECRET, "$1 [redacted]");
22
+ }
23
+ if (value === null || typeof value !== "object")
24
+ return value;
25
+ if (seen.has(value))
26
+ return "[circular]";
27
+ seen.add(value);
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
+ }
41
+ function redactArguments(args, redactValue) {
42
+ const output = [];
43
+ let redactNext = false;
44
+ for (const arg of args) {
45
+ if (redactNext) {
46
+ output.push("[redacted]");
47
+ redactNext = false;
48
+ continue;
49
+ }
50
+ if (typeof arg === "string") {
51
+ const equals = /^(--(?:password|passwd|token|api-key|api_key|secret|client-secret))=(.*)$/i.exec(arg);
52
+ if (equals) {
53
+ output.push(`${equals[1]}=[redacted]`);
54
+ continue;
55
+ }
56
+ if (/^--(?:password|passwd|token|api-key|api_key|secret|client-secret)$/i.test(arg)) {
57
+ output.push(arg);
58
+ redactNext = true;
59
+ continue;
60
+ }
61
+ }
62
+ output.push(redactValue(arg));
63
+ }
64
+ return output;
65
+ }
@@ -0,0 +1,19 @@
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
+ workspaceId?: string;
8
+ }
9
+ export interface OperationRepository {
10
+ insert(operation: Operation): void;
11
+ update(operation: Operation): void;
12
+ get(operationId: string): Operation | null;
13
+ list(query?: OperationQuery): Operation[];
14
+ clearHistory(): {
15
+ deleted: number;
16
+ preserved: number;
17
+ };
18
+ reconcileRunning(finishedAt: string): number;
19
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,31 @@
1
+ /** Persistent plugin operation lifecycle recorded by ChatRoom. */
2
+ export declare const OPERATION_STATUSES: readonly ["running", "success", "error", "cancelled"];
3
+ export type OperationStatus = (typeof OPERATION_STATUSES)[number];
4
+ /** Where an operation was initiated; pluginId identifies which plugin owns the operation. */
5
+ export declare const OPERATION_SOURCES: readonly ["mcp", "gui", "system", "cli"];
6
+ export type OperationSource = (typeof OPERATION_SOURCES)[number];
7
+ export interface Operation {
8
+ operationId: string;
9
+ pluginId: string;
10
+ source: OperationSource;
11
+ action: string;
12
+ status: OperationStatus;
13
+ workspaceId: string | null;
14
+ processId: string | null;
15
+ input: unknown;
16
+ output: unknown;
17
+ error: unknown;
18
+ inputTruncated: boolean;
19
+ outputTruncated: boolean;
20
+ startedAt: string;
21
+ finishedAt: string | null;
22
+ durationMs: number | null;
23
+ }
24
+ export interface OperationStart {
25
+ pluginId: string;
26
+ source: OperationSource;
27
+ action: string;
28
+ input?: unknown;
29
+ workspaceId?: string | null;
30
+ processId?: string | null;
31
+ }
@@ -0,0 +1,9 @@
1
+ /** Persistent plugin operation lifecycle recorded by ChatRoom. */
2
+ export const OPERATION_STATUSES = [
3
+ "running",
4
+ "success",
5
+ "error",
6
+ "cancelled",
7
+ ];
8
+ /** Where an operation was initiated; pluginId identifies which plugin owns the operation. */
9
+ export const OPERATION_SOURCES = ["mcp", "gui", "system", "cli"];
@@ -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
+ }
@@ -0,0 +1,15 @@
1
+ export interface CommandRequest {
2
+ command: string;
3
+ args?: string[];
4
+ cwd?: string;
5
+ env?: Record<string, string>;
6
+ timeoutMs?: number;
7
+ }
8
+ export interface CommandResult {
9
+ stdout: string;
10
+ stderr: string;
11
+ exitCode: number;
12
+ }
13
+ export declare class CommandRunner {
14
+ run(request: CommandRequest): Promise<CommandResult>;
15
+ }