@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,112 @@
1
+ import { z } from "zod";
2
+ import { CLOUD_ENTITLEMENT_SCHEMA, CLOUD_HTTP_URL_SCHEMA, CLOUD_LEASE_SCHEMA, } from "./types.js";
3
+ import { signDeviceProof } from "./device-proof.js";
4
+ const customerSchema = z.object({ id: z.string().min(1) }).strict();
5
+ const recoveryCredentialSchema = z
6
+ .object({
7
+ recoveryKey: z.string().regex(/^crr\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/),
8
+ })
9
+ .strict();
10
+ const sessionSchema = z
11
+ .object({
12
+ purchaseToken: z.string().min(24),
13
+ managementUrl: CLOUD_HTTP_URL_SCHEMA,
14
+ expiresAt: z.string().datetime(),
15
+ })
16
+ .strict();
17
+ const statusSchema = z
18
+ .object({
19
+ managementSessionActive: z.boolean(),
20
+ entitlements: z.array(CLOUD_ENTITLEMENT_SCHEMA),
21
+ customer: customerSchema.nullable(),
22
+ publicPrefix: z.string().min(1).nullable(),
23
+ })
24
+ .strict();
25
+ const restoreSchema = z
26
+ .object({
27
+ entitlements: z.array(CLOUD_ENTITLEMENT_SCHEMA),
28
+ customer: customerSchema,
29
+ publicPrefix: z.string().min(1).nullable(),
30
+ recoveryKey: z.string().regex(/^crr\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/),
31
+ })
32
+ .strict();
33
+ export class CloudApiClient {
34
+ baseUrl;
35
+ constructor(baseUrl) {
36
+ this.baseUrl = baseUrl;
37
+ }
38
+ async createManagementSession(identity) {
39
+ const payload = { devicePublicKey: identity.devicePublicKey };
40
+ return sessionSchema.parse(await this.devicePost("/v1/device/session", "session", identity, payload));
41
+ }
42
+ async status(identity, purchaseToken) {
43
+ const payload = {
44
+ devicePublicKey: identity.devicePublicKey,
45
+ purchaseToken,
46
+ };
47
+ return statusSchema.parse(await this.devicePost("/v1/device/status", "status", identity, payload));
48
+ }
49
+ async restore(identity, recoveryKey) {
50
+ const payload = { devicePublicKey: identity.devicePublicKey, recoveryKey };
51
+ return restoreSchema.parse(await this.devicePost("/v1/device/restore", "restore", identity, payload));
52
+ }
53
+ async replaceRecoveryKey(identity) {
54
+ const payload = { devicePublicKey: identity.devicePublicKey };
55
+ return recoveryCredentialSchema.parse(await this.devicePost("/v1/device/recovery-key", "recovery-key", identity, payload));
56
+ }
57
+ async lease(identity, requestedServices) {
58
+ const payload = {
59
+ devicePublicKey: identity.devicePublicKey,
60
+ requestedServices,
61
+ };
62
+ return CLOUD_LEASE_SCHEMA.parse(await this.devicePost("/v1/device/lease", "lease", identity, payload));
63
+ }
64
+ async devicePost(path, operation, identity, payload) {
65
+ const proof = signDeviceProof({
66
+ operation,
67
+ installationId: identity.installationId,
68
+ devicePrivateKey: identity.devicePrivateKey,
69
+ payload,
70
+ });
71
+ const response = await fetch(new URL(path, `${this.baseUrl}/`), {
72
+ method: "POST",
73
+ headers: {
74
+ "content-type": "application/json",
75
+ accept: "application/json",
76
+ },
77
+ body: JSON.stringify({
78
+ installationId: identity.installationId,
79
+ ...payload,
80
+ ...proof,
81
+ }),
82
+ signal: AbortSignal.timeout(15_000),
83
+ });
84
+ const body = (await response.json().catch(() => null));
85
+ if (!response.ok) {
86
+ const message = typeof body === "object" &&
87
+ body &&
88
+ "message" in body &&
89
+ typeof body.message === "string"
90
+ ? body.message
91
+ : `ChatRoom Cloud request failed (${response.status})`;
92
+ const issues = typeof body === "object" &&
93
+ body &&
94
+ "issues" in body &&
95
+ Array.isArray(body.issues)
96
+ ? body.issues.flatMap((issue) => {
97
+ if (!issue || typeof issue !== "object")
98
+ return [];
99
+ const path = "path" in issue && Array.isArray(issue.path)
100
+ ? issue.path.join(".")
101
+ : "";
102
+ const detail = "message" in issue && typeof issue.message === "string"
103
+ ? issue.message
104
+ : "";
105
+ return detail ? [`${path || "request"}: ${detail}`] : [];
106
+ })
107
+ : [];
108
+ throw new Error(issues.length ? `${message}: ${issues.join("; ")}` : message);
109
+ }
110
+ return body;
111
+ }
112
+ }
@@ -0,0 +1,58 @@
1
+ import type { ChatRoomConfig } from "#config/types";
2
+ import type { LogWriter } from "#core/logging/types";
3
+ import { type CloudServiceId, type CloudStatus } from "./types.js";
4
+ interface ExternalAccessSink {
5
+ setCloud(state: {
6
+ mcpBaseUrl: string | null;
7
+ webBaseUrl: string | null;
8
+ }): void;
9
+ clearCloud(): void;
10
+ }
11
+ export declare class CloudController {
12
+ private readonly config;
13
+ private readonly logs;
14
+ private readonly externalAccess;
15
+ private readonly store;
16
+ private readonly api;
17
+ private state;
18
+ private tunnel;
19
+ private connection;
20
+ private lastError;
21
+ private managementTimer;
22
+ private renewalTimer;
23
+ private statusRetryTimer;
24
+ private stopped;
25
+ private mutationQueue;
26
+ private constructor();
27
+ static create(config: ChatRoomConfig, logs: LogWriter, externalAccess: ExternalAccessSink): Promise<CloudController>;
28
+ status(): CloudStatus;
29
+ start(): Promise<void>;
30
+ stop(): Promise<void>;
31
+ managementUrl(): Promise<{
32
+ url: string;
33
+ expiresAt: string;
34
+ }>;
35
+ syncStatus(): Promise<CloudStatus>;
36
+ restore(recoveryKey: string): Promise<{
37
+ status: CloudStatus;
38
+ recoveryKey: string;
39
+ }>;
40
+ replaceRecoveryKey(): Promise<string>;
41
+ setService(service: CloudServiceId, enabled: boolean): Promise<CloudStatus>;
42
+ private syncStatusInternal;
43
+ private refreshLeaseIfNeeded;
44
+ private connectTunnel;
45
+ private recoverRejectedLease;
46
+ private publishExternalAccess;
47
+ private scheduleStatusRetry;
48
+ private scheduleManagementSync;
49
+ private scheduleRenewal;
50
+ private clearTimers;
51
+ private serial;
52
+ private validManagementSession;
53
+ private identity;
54
+ private ensureState;
55
+ private requireState;
56
+ private setError;
57
+ }
58
+ export {};
@@ -0,0 +1,425 @@
1
+ import { ChatRoomError } from "#core/errors/chatroom-error";
2
+ import { CloudApiClient } from "./api-client.js";
3
+ import { CloudStateStore } from "./state-store.js";
4
+ import { CloudTunnelClient } from "./tunnel-client.js";
5
+ import { CLOUD_SERVICES, } from "./types.js";
6
+ export class CloudController {
7
+ config;
8
+ logs;
9
+ externalAccess;
10
+ store;
11
+ api;
12
+ state = null;
13
+ tunnel = null;
14
+ connection = "inactive";
15
+ lastError = null;
16
+ managementTimer = null;
17
+ renewalTimer = null;
18
+ statusRetryTimer = null;
19
+ stopped = true;
20
+ mutationQueue = Promise.resolve();
21
+ constructor(config, cloudApi, logs, externalAccess) {
22
+ this.config = config;
23
+ this.logs = logs;
24
+ this.externalAccess = externalAccess;
25
+ this.store = new CloudStateStore(config.dataDir);
26
+ this.api = new CloudApiClient(cloudApi);
27
+ }
28
+ static async create(config, logs, externalAccess) {
29
+ const apiBaseUrl = (process.env.CHATROOM_CLOUD_API ?? "https://chatroomcp.com").replace(/\/$/, "");
30
+ const controller = new CloudController(config, apiBaseUrl, logs, externalAccess);
31
+ await controller.ensureState().catch(() => undefined);
32
+ return controller;
33
+ }
34
+ status() {
35
+ const state = this.state;
36
+ if (!state) {
37
+ return {
38
+ installationId: null,
39
+ customerId: null,
40
+ publicPrefix: null,
41
+ desiredServices: { remote_mcp: false, remote_web: false },
42
+ entitlements: [],
43
+ managementSessionActive: false,
44
+ connection: "error",
45
+ mcpUrl: null,
46
+ webUrl: null,
47
+ lastError: this.lastError ?? "ChatRoom Cloud is unavailable",
48
+ };
49
+ }
50
+ const lease = state.lease && Date.parse(state.lease.expiresAt) > Date.now()
51
+ ? state.lease
52
+ : null;
53
+ return {
54
+ installationId: state.installationId,
55
+ customerId: state.customerId,
56
+ publicPrefix: state.publicPrefix,
57
+ desiredServices: { ...state.desiredServices },
58
+ entitlements: [...state.entitlements],
59
+ managementSessionActive: Boolean(state.managementSession &&
60
+ Date.parse(state.managementSession.expiresAt) > Date.now()),
61
+ connection: this.connection,
62
+ mcpUrl: lease?.mcpBaseUrl ? `${lease.mcpBaseUrl}/mcp` : null,
63
+ webUrl: lease?.webBaseUrl ?? null,
64
+ lastError: this.lastError,
65
+ };
66
+ }
67
+ start() {
68
+ return this.serial(async () => {
69
+ if (!this.stopped)
70
+ return;
71
+ this.stopped = false;
72
+ if (!this.state)
73
+ return;
74
+ await this.syncStatusInternal().catch((error) => {
75
+ this.setError(error);
76
+ this.scheduleStatusRetry();
77
+ });
78
+ this.scheduleManagementSync();
79
+ });
80
+ }
81
+ stop() {
82
+ this.stopped = true;
83
+ this.clearTimers();
84
+ return this.serial(async () => {
85
+ this.tunnel?.stop();
86
+ this.tunnel = null;
87
+ this.externalAccess.clearCloud();
88
+ if (this.state?.lease)
89
+ this.connection = "disconnected";
90
+ });
91
+ }
92
+ managementUrl() {
93
+ return this.serial(async () => {
94
+ const state = await this.ensureState();
95
+ const current = state.managementSession;
96
+ if (current && Date.parse(current.expiresAt) > Date.now()) {
97
+ return { url: current.managementUrl, expiresAt: current.expiresAt };
98
+ }
99
+ const session = await this.api.createManagementSession(this.identity());
100
+ this.state = {
101
+ ...state,
102
+ managementSession: {
103
+ purchaseToken: session.purchaseToken,
104
+ managementUrl: session.managementUrl,
105
+ expiresAt: session.expiresAt,
106
+ },
107
+ };
108
+ await this.store.save(this.state);
109
+ this.scheduleManagementSync(0);
110
+ return { url: session.managementUrl, expiresAt: session.expiresAt };
111
+ });
112
+ }
113
+ syncStatus() {
114
+ return this.serial(() => this.syncStatusInternal());
115
+ }
116
+ restore(recoveryKey) {
117
+ return this.serial(async () => {
118
+ const state = await this.ensureState();
119
+ const previousEntitlements = state.entitlements;
120
+ const result = await this.api.restore(this.identity(), recoveryKey);
121
+ this.state = {
122
+ ...state,
123
+ customerId: result.customer.id,
124
+ publicPrefix: result.publicPrefix,
125
+ desiredServices: enableNewlyPurchasedServices(state.desiredServices, previousEntitlements.map((entry) => entry.service), result.entitlements.map((entry) => entry.service)),
126
+ entitlements: result.entitlements,
127
+ lease: null,
128
+ };
129
+ await this.store.save(this.state);
130
+ this.tunnel?.stop();
131
+ this.tunnel = null;
132
+ if (!this.stopped) {
133
+ await this.refreshLeaseIfNeeded(true);
134
+ this.connectTunnel();
135
+ this.scheduleRenewal();
136
+ }
137
+ this.lastError = null;
138
+ return { status: this.status(), recoveryKey: result.recoveryKey };
139
+ });
140
+ }
141
+ async replaceRecoveryKey() {
142
+ await this.ensureState();
143
+ const result = await this.api.replaceRecoveryKey(this.identity());
144
+ return result.recoveryKey;
145
+ }
146
+ setService(service, enabled) {
147
+ return this.serial(async () => {
148
+ const state = await this.ensureState();
149
+ if (!CLOUD_SERVICES.includes(service))
150
+ throw new Error(`Unknown Cloud service: ${service}`);
151
+ this.state = {
152
+ ...state,
153
+ desiredServices: { ...state.desiredServices, [service]: enabled },
154
+ };
155
+ await this.store.save(this.state);
156
+ if (!this.stopped) {
157
+ await this.refreshLeaseIfNeeded(true);
158
+ this.connectTunnel();
159
+ this.scheduleRenewal();
160
+ }
161
+ this.lastError = null;
162
+ return this.status();
163
+ });
164
+ }
165
+ async syncStatusInternal() {
166
+ const state = await this.ensureState();
167
+ const managementSession = this.validManagementSession();
168
+ const previousEntitlements = state.entitlements;
169
+ const previousPrefix = state.publicPrefix;
170
+ const result = await this.api.status(this.identity(), managementSession?.purchaseToken ?? null);
171
+ this.state = {
172
+ ...state,
173
+ customerId: result.customer?.id ?? null,
174
+ publicPrefix: result.publicPrefix,
175
+ desiredServices: enableNewlyPurchasedServices(state.desiredServices, previousEntitlements.map((entry) => entry.service), result.entitlements.map((entry) => entry.service)),
176
+ entitlements: result.entitlements,
177
+ managementSession: result.managementSessionActive
178
+ ? managementSession
179
+ : null,
180
+ };
181
+ await this.store.save(this.state);
182
+ if (!this.stopped) {
183
+ await this.refreshLeaseIfNeeded(previousPrefix !== null && previousPrefix !== result.publicPrefix);
184
+ this.connectTunnel();
185
+ this.scheduleRenewal();
186
+ }
187
+ this.lastError = null;
188
+ if (this.statusRetryTimer)
189
+ clearTimeout(this.statusRetryTimer);
190
+ this.statusRetryTimer = null;
191
+ return this.status();
192
+ }
193
+ async refreshLeaseIfNeeded(force) {
194
+ const state = this.requireState();
195
+ const entitled = new Set(state.entitlements.map((entry) => entry.service));
196
+ const requested = CLOUD_SERVICES.filter((service) => state.desiredServices[service] && entitled.has(service));
197
+ if (requested.length === 0) {
198
+ if (state.lease) {
199
+ this.state = { ...state, lease: null };
200
+ await this.store.save(this.state);
201
+ }
202
+ this.tunnel?.drainAndStop();
203
+ this.tunnel = null;
204
+ this.connection = "inactive";
205
+ this.publishExternalAccess();
206
+ return;
207
+ }
208
+ if (!state.publicPrefix) {
209
+ if (state.lease) {
210
+ this.state = { ...state, lease: null };
211
+ await this.store.save(this.state);
212
+ }
213
+ this.tunnel?.drainAndStop();
214
+ this.tunnel = null;
215
+ this.connection = "inactive";
216
+ this.publishExternalAccess();
217
+ return;
218
+ }
219
+ const current = state.lease;
220
+ const sameServices = current &&
221
+ requested.length === current.services.length &&
222
+ requested.every((service) => current.services.includes(service));
223
+ const fresh = current && Date.parse(current.expiresAt) - Date.now() > 15 * 60 * 1000;
224
+ if (!force && sameServices && fresh) {
225
+ this.publishExternalAccess();
226
+ return;
227
+ }
228
+ const lease = await this.api.lease(this.identity(), requested);
229
+ this.state = { ...state, lease };
230
+ await this.store.save(this.state);
231
+ this.publishExternalAccess();
232
+ this.tunnel?.updateLease(lease);
233
+ }
234
+ connectTunnel() {
235
+ const state = this.state;
236
+ if (this.stopped ||
237
+ this.tunnel ||
238
+ !state?.lease ||
239
+ state.lease.services.length === 0)
240
+ return;
241
+ this.connection = "connecting";
242
+ this.logs.info("cloud", "cloud.connecting", "Cloud tunnel is connecting", {
243
+ services: state.lease.services,
244
+ });
245
+ let tunnel;
246
+ tunnel = new CloudTunnelClient(state.lease, { devicePrivateKey: state.devicePrivateKey }, this.config.server, {
247
+ onConnected: () => {
248
+ this.lastError = null;
249
+ this.connection = "connected";
250
+ this.logs.info("cloud", "cloud.connected", "Cloud tunnel connected", {
251
+ services: this.state?.lease?.services ?? [],
252
+ });
253
+ },
254
+ onDisconnected: () => {
255
+ if (!this.stopped && this.state?.lease) {
256
+ this.connection = "disconnected";
257
+ this.logs.warn("cloud", "cloud.disconnected", "Cloud tunnel disconnected");
258
+ }
259
+ },
260
+ onLeaseRejected: () => {
261
+ void this.recoverRejectedLease(tunnel);
262
+ },
263
+ onError: (error) => this.setError(error),
264
+ });
265
+ this.tunnel = tunnel;
266
+ tunnel.start();
267
+ }
268
+ recoverRejectedLease(tunnel) {
269
+ return this.serial(async () => {
270
+ if (this.stopped || this.tunnel !== tunnel || !this.state)
271
+ return;
272
+ tunnel.stop();
273
+ this.tunnel = null;
274
+ this.state = { ...this.state, lease: null };
275
+ await this.store.save(this.state);
276
+ this.connection = "connecting";
277
+ this.publishExternalAccess();
278
+ this.logs.warn("cloud", "cloud.lease-rejected", "Cloud lease was rejected; requesting a replacement");
279
+ try {
280
+ await this.refreshLeaseIfNeeded(true);
281
+ this.connectTunnel();
282
+ this.scheduleRenewal();
283
+ }
284
+ catch (error) {
285
+ this.setError(error);
286
+ this.scheduleStatusRetry();
287
+ }
288
+ });
289
+ }
290
+ publishExternalAccess() {
291
+ const state = this.state;
292
+ if (!state) {
293
+ this.externalAccess.clearCloud();
294
+ return;
295
+ }
296
+ const lease = state.lease;
297
+ const activeLease = lease && Date.parse(lease.expiresAt) > Date.now() ? lease : null;
298
+ this.externalAccess.setCloud({
299
+ mcpBaseUrl: activeLease?.services.includes("remote_mcp")
300
+ ? activeLease.mcpBaseUrl
301
+ : null,
302
+ webBaseUrl: activeLease?.services.includes("remote_web")
303
+ ? activeLease.webBaseUrl
304
+ : null,
305
+ });
306
+ }
307
+ scheduleStatusRetry(delayMs = 30_000) {
308
+ if (this.stopped || !this.state || this.statusRetryTimer)
309
+ return;
310
+ this.statusRetryTimer = setTimeout(() => {
311
+ this.statusRetryTimer = null;
312
+ void this.syncStatus().catch((error) => {
313
+ this.setError(error);
314
+ this.scheduleStatusRetry();
315
+ });
316
+ }, delayMs);
317
+ this.statusRetryTimer.unref();
318
+ }
319
+ scheduleManagementSync(delayMs = 3_000) {
320
+ if (this.managementTimer)
321
+ clearTimeout(this.managementTimer);
322
+ this.managementTimer = null;
323
+ if (this.stopped || !this.validManagementSession())
324
+ return;
325
+ this.managementTimer = setTimeout(() => {
326
+ this.managementTimer = null;
327
+ void this.syncStatus()
328
+ .catch((error) => this.setError(error))
329
+ .finally(() => this.scheduleManagementSync());
330
+ }, delayMs);
331
+ this.managementTimer.unref();
332
+ }
333
+ scheduleRenewal() {
334
+ if (this.renewalTimer)
335
+ clearTimeout(this.renewalTimer);
336
+ this.renewalTimer = null;
337
+ const lease = this.state?.lease;
338
+ if (this.stopped || !lease)
339
+ return;
340
+ const remaining = Date.parse(lease.expiresAt) - Date.now();
341
+ const delay = Math.max(60_000, remaining - (10 + Math.random() * 10) * 60_000);
342
+ this.renewalTimer = setTimeout(() => {
343
+ this.renewalTimer = null;
344
+ void this.serial(async () => {
345
+ if (this.stopped)
346
+ return;
347
+ await this.refreshLeaseIfNeeded(true);
348
+ this.connectTunnel();
349
+ })
350
+ .catch((error) => this.setError(error))
351
+ .finally(() => this.scheduleRenewal());
352
+ }, delay);
353
+ this.renewalTimer.unref();
354
+ }
355
+ clearTimers() {
356
+ if (this.managementTimer)
357
+ clearTimeout(this.managementTimer);
358
+ if (this.renewalTimer)
359
+ clearTimeout(this.renewalTimer);
360
+ if (this.statusRetryTimer)
361
+ clearTimeout(this.statusRetryTimer);
362
+ this.managementTimer = null;
363
+ this.renewalTimer = null;
364
+ this.statusRetryTimer = null;
365
+ }
366
+ serial(action) {
367
+ const next = this.mutationQueue.then(action, action);
368
+ this.mutationQueue = next.then(() => undefined, () => undefined);
369
+ return next;
370
+ }
371
+ validManagementSession() {
372
+ const value = this.state?.managementSession;
373
+ return value && Date.parse(value.expiresAt) > Date.now() ? value : null;
374
+ }
375
+ identity() {
376
+ const state = this.requireState();
377
+ return {
378
+ installationId: state.installationId,
379
+ devicePublicKey: state.devicePublicKey,
380
+ devicePrivateKey: state.devicePrivateKey,
381
+ };
382
+ }
383
+ async ensureState() {
384
+ if (this.state)
385
+ return this.state;
386
+ try {
387
+ this.state = await this.store.loadOrCreate();
388
+ this.lastError = null;
389
+ this.connection = "inactive";
390
+ this.publishExternalAccess();
391
+ return this.state;
392
+ }
393
+ catch (error) {
394
+ this.externalAccess.clearCloud();
395
+ const detail = error instanceof Error ? error.message : String(error);
396
+ this.lastError = `Cloud state unavailable: ${detail}`;
397
+ this.connection = "error";
398
+ this.logs.error("cloud", "cloud.state_unavailable", "Cloud state is unavailable", { error });
399
+ throw new ChatRoomError("INTERNAL", "ChatRoom Cloud is unavailable", { reason: "cloud_state_unavailable" }, error instanceof Error ? { cause: error } : undefined);
400
+ }
401
+ }
402
+ requireState() {
403
+ if (!this.state)
404
+ throw new ChatRoomError("INTERNAL", "ChatRoom Cloud is unavailable", {
405
+ reason: "cloud_state_unavailable",
406
+ });
407
+ return this.state;
408
+ }
409
+ setError(error) {
410
+ this.lastError = error instanceof Error ? error.message : String(error);
411
+ this.connection = "error";
412
+ this.logs.error("cloud", "cloud.error", "Cloud operation failed", {
413
+ error,
414
+ });
415
+ }
416
+ }
417
+ function enableNewlyPurchasedServices(desiredServices, previousEntitlements, currentEntitlements) {
418
+ const previous = new Set(previousEntitlements);
419
+ const current = new Set(currentEntitlements);
420
+ const next = { ...desiredServices };
421
+ for (const service of CLOUD_SERVICES)
422
+ if (!previous.has(service) && current.has(service))
423
+ next[service] = true;
424
+ return next;
425
+ }
@@ -0,0 +1,9 @@
1
+ export declare function signDeviceProof(input: {
2
+ operation: string;
3
+ installationId: string;
4
+ devicePrivateKey: string;
5
+ payload: unknown;
6
+ }): {
7
+ timestamp: string;
8
+ signature: string;
9
+ };
@@ -0,0 +1,17 @@
1
+ import { createHash, createPrivateKey, sign } from "node:crypto";
2
+ export function signDeviceProof(input) {
3
+ const timestamp = new Date().toISOString();
4
+ const payloadHash = createHash("sha256")
5
+ .update(JSON.stringify(input.payload))
6
+ .digest("base64url");
7
+ const message = Buffer.from(`chatroom-cloud-device-v1\n${input.operation}\n${input.installationId}\n${timestamp}\n${payloadHash}`);
8
+ const key = createPrivateKey({
9
+ key: Buffer.from(input.devicePrivateKey, "base64url"),
10
+ format: "der",
11
+ type: "pkcs8",
12
+ });
13
+ return {
14
+ timestamp,
15
+ signature: sign(null, message, key).toString("base64url"),
16
+ };
17
+ }
@@ -0,0 +1,4 @@
1
+ import type { InternalPlugin } from "#plugins/types";
2
+ import { CloudController } from "./controller.js";
3
+ export declare const CloudService: import("#plugins/types").ServiceToken<CloudController>;
4
+ export declare function createCloudPlugin(): InternalPlugin;
@@ -0,0 +1,17 @@
1
+ import { createServiceToken } from "#plugins/types";
2
+ import { CloudController } from "./controller.js";
3
+ export const CloudService = createServiceToken("cloud");
4
+ export function createCloudPlugin() {
5
+ let controller = null;
6
+ return {
7
+ id: "cloud",
8
+ async activate(context) {
9
+ controller = await CloudController.create(context.config, context.logs, context.externalAccess);
10
+ context.services.provide(CloudService, controller);
11
+ },
12
+ async deactivate() {
13
+ await controller?.stop();
14
+ controller = null;
15
+ },
16
+ };
17
+ }
@@ -0,0 +1,7 @@
1
+ import { type CloudPersistedState } from "./types.js";
2
+ export declare class CloudStateStore {
3
+ readonly filePath: string;
4
+ constructor(dataDir: string);
5
+ loadOrCreate(): Promise<CloudPersistedState>;
6
+ save(state: CloudPersistedState): Promise<void>;
7
+ }