@opengsd/gsd-pi 1.0.2-dev.50223bc → 1.0.2-dev.5961fbf

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 (251) hide show
  1. package/dist/resource-loader.d.ts +5 -0
  2. package/dist/resource-loader.js +24 -8
  3. package/dist/resources/.managed-resources-content-hash +1 -1
  4. package/dist/resources/extensions/gsd/auto/loop.js +19 -0
  5. package/dist/resources/extensions/gsd/auto/phases.js +1 -1
  6. package/dist/resources/extensions/gsd/auto-worktree.js +2 -54
  7. package/dist/resources/extensions/gsd/worktree-post-create-hook.js +117 -0
  8. package/dist/web/standalone/.next/BUILD_ID +1 -1
  9. package/dist/web/standalone/.next/app-path-routes-manifest.json +11 -11
  10. package/dist/web/standalone/.next/build-manifest.json +2 -2
  11. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  12. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  13. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  14. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  15. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  16. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  17. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  18. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  19. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  20. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  21. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  22. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  23. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  24. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  25. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  26. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  27. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  28. package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
  29. package/dist/web/standalone/.next/server/app/api/session/events/route.js +1 -1
  30. package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
  31. package/dist/web/standalone/.next/server/app/index.html +1 -1
  32. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app-paths-manifest.json +11 -11
  39. package/dist/web/standalone/.next/server/chunks/1834.js +1 -1
  40. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  41. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  42. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  43. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  44. package/dist/web/standalone/node_modules/node-pty/build/Makefile +1 -1
  45. package/dist/web/standalone/package.json +0 -1
  46. package/dist/worktree-cli.d.ts +0 -2
  47. package/dist/worktree-cli.js +21 -9
  48. package/package.json +9 -4
  49. package/packages/cloud-mcp-gateway/bin/gsd-cloud-mcp-gateway.js +14 -0
  50. package/packages/cloud-mcp-gateway/package.json +5 -4
  51. package/packages/contracts/package.json +2 -2
  52. package/packages/daemon/bin/gsd-daemon.js +14 -0
  53. package/packages/daemon/bin/gsd-mcp-runtime.js +14 -0
  54. package/packages/daemon/bin/gsd-mcp.js +14 -0
  55. package/packages/daemon/dist/channel-manager.d.ts +53 -0
  56. package/packages/daemon/dist/channel-manager.d.ts.map +1 -0
  57. package/packages/daemon/dist/channel-manager.js +167 -0
  58. package/packages/daemon/dist/channel-manager.js.map +1 -0
  59. package/packages/daemon/dist/cli.d.ts +3 -0
  60. package/packages/daemon/dist/cli.d.ts.map +1 -0
  61. package/packages/daemon/dist/cli.js +94 -0
  62. package/packages/daemon/dist/cli.js.map +1 -0
  63. package/packages/daemon/dist/cloud-cli.d.ts +7 -0
  64. package/packages/daemon/dist/cloud-cli.d.ts.map +1 -0
  65. package/packages/daemon/dist/cloud-cli.js +96 -0
  66. package/packages/daemon/dist/cloud-cli.js.map +1 -0
  67. package/packages/daemon/dist/cloud-config.d.ts +18 -0
  68. package/packages/daemon/dist/cloud-config.d.ts.map +1 -0
  69. package/packages/daemon/dist/cloud-config.js +209 -0
  70. package/packages/daemon/dist/cloud-config.js.map +1 -0
  71. package/packages/daemon/dist/cloud-config.test.d.ts +2 -0
  72. package/packages/daemon/dist/cloud-config.test.d.ts.map +1 -0
  73. package/packages/daemon/dist/cloud-config.test.js +132 -0
  74. package/packages/daemon/dist/cloud-config.test.js.map +1 -0
  75. package/packages/daemon/dist/cloud-runtime.d.ts +26 -0
  76. package/packages/daemon/dist/cloud-runtime.d.ts.map +1 -0
  77. package/packages/daemon/dist/cloud-runtime.js +180 -0
  78. package/packages/daemon/dist/cloud-runtime.js.map +1 -0
  79. package/packages/daemon/dist/cloud-runtime.test.d.ts +2 -0
  80. package/packages/daemon/dist/cloud-runtime.test.d.ts.map +1 -0
  81. package/packages/daemon/dist/cloud-runtime.test.js +28 -0
  82. package/packages/daemon/dist/cloud-runtime.test.js.map +1 -0
  83. package/packages/daemon/dist/cloud-token.d.ts +3 -0
  84. package/packages/daemon/dist/cloud-token.d.ts.map +1 -0
  85. package/packages/daemon/dist/cloud-token.js +37 -0
  86. package/packages/daemon/dist/cloud-token.js.map +1 -0
  87. package/packages/daemon/dist/commands.d.ts +25 -0
  88. package/packages/daemon/dist/commands.d.ts.map +1 -0
  89. package/packages/daemon/dist/commands.js +81 -0
  90. package/packages/daemon/dist/commands.js.map +1 -0
  91. package/packages/daemon/dist/config.d.ts +17 -0
  92. package/packages/daemon/dist/config.d.ts.map +1 -0
  93. package/packages/daemon/dist/config.js +146 -0
  94. package/packages/daemon/dist/config.js.map +1 -0
  95. package/packages/daemon/dist/daemon.d.ts +38 -0
  96. package/packages/daemon/dist/daemon.d.ts.map +1 -0
  97. package/packages/daemon/dist/daemon.js +194 -0
  98. package/packages/daemon/dist/daemon.js.map +1 -0
  99. package/packages/daemon/dist/daemon.test.d.ts +2 -0
  100. package/packages/daemon/dist/daemon.test.d.ts.map +1 -0
  101. package/packages/daemon/dist/daemon.test.js +692 -0
  102. package/packages/daemon/dist/daemon.test.js.map +1 -0
  103. package/packages/daemon/dist/discord-bot.d.ts +70 -0
  104. package/packages/daemon/dist/discord-bot.d.ts.map +1 -0
  105. package/packages/daemon/dist/discord-bot.js +433 -0
  106. package/packages/daemon/dist/discord-bot.js.map +1 -0
  107. package/packages/daemon/dist/discord-bot.test.d.ts +2 -0
  108. package/packages/daemon/dist/discord-bot.test.d.ts.map +1 -0
  109. package/packages/daemon/dist/discord-bot.test.js +667 -0
  110. package/packages/daemon/dist/discord-bot.test.js.map +1 -0
  111. package/packages/daemon/dist/event-bridge.d.ts +72 -0
  112. package/packages/daemon/dist/event-bridge.d.ts.map +1 -0
  113. package/packages/daemon/dist/event-bridge.js +366 -0
  114. package/packages/daemon/dist/event-bridge.js.map +1 -0
  115. package/packages/daemon/dist/event-bridge.test.d.ts +9 -0
  116. package/packages/daemon/dist/event-bridge.test.d.ts.map +1 -0
  117. package/packages/daemon/dist/event-bridge.test.js +528 -0
  118. package/packages/daemon/dist/event-bridge.test.js.map +1 -0
  119. package/packages/daemon/dist/event-formatter.d.ts +34 -0
  120. package/packages/daemon/dist/event-formatter.d.ts.map +1 -0
  121. package/packages/daemon/dist/event-formatter.js +355 -0
  122. package/packages/daemon/dist/event-formatter.js.map +1 -0
  123. package/packages/daemon/dist/event-formatter.test.d.ts +2 -0
  124. package/packages/daemon/dist/event-formatter.test.d.ts.map +1 -0
  125. package/packages/daemon/dist/event-formatter.test.js +333 -0
  126. package/packages/daemon/dist/event-formatter.test.js.map +1 -0
  127. package/packages/daemon/dist/index.d.ts +25 -0
  128. package/packages/daemon/dist/index.d.ts.map +1 -0
  129. package/packages/daemon/dist/index.js +17 -0
  130. package/packages/daemon/dist/index.js.map +1 -0
  131. package/packages/daemon/dist/launchd.d.ts +49 -0
  132. package/packages/daemon/dist/launchd.d.ts.map +1 -0
  133. package/packages/daemon/dist/launchd.js +188 -0
  134. package/packages/daemon/dist/launchd.js.map +1 -0
  135. package/packages/daemon/dist/launchd.test.d.ts +2 -0
  136. package/packages/daemon/dist/launchd.test.d.ts.map +1 -0
  137. package/packages/daemon/dist/launchd.test.js +296 -0
  138. package/packages/daemon/dist/launchd.test.js.map +1 -0
  139. package/packages/daemon/dist/local-tool-executor.d.ts +22 -0
  140. package/packages/daemon/dist/local-tool-executor.d.ts.map +1 -0
  141. package/packages/daemon/dist/local-tool-executor.js +307 -0
  142. package/packages/daemon/dist/local-tool-executor.js.map +1 -0
  143. package/packages/daemon/dist/local-tool-executor.test.d.ts +2 -0
  144. package/packages/daemon/dist/local-tool-executor.test.d.ts.map +1 -0
  145. package/packages/daemon/dist/local-tool-executor.test.js +111 -0
  146. package/packages/daemon/dist/local-tool-executor.test.js.map +1 -0
  147. package/packages/daemon/dist/logger.d.ts +25 -0
  148. package/packages/daemon/dist/logger.d.ts.map +1 -0
  149. package/packages/daemon/dist/logger.js +72 -0
  150. package/packages/daemon/dist/logger.js.map +1 -0
  151. package/packages/daemon/dist/mcp-cli.d.ts +3 -0
  152. package/packages/daemon/dist/mcp-cli.d.ts.map +1 -0
  153. package/packages/daemon/dist/mcp-cli.js +8 -0
  154. package/packages/daemon/dist/mcp-cli.js.map +1 -0
  155. package/packages/daemon/dist/mcp-cli.test.d.ts +2 -0
  156. package/packages/daemon/dist/mcp-cli.test.d.ts.map +1 -0
  157. package/packages/daemon/dist/mcp-cli.test.js +13 -0
  158. package/packages/daemon/dist/mcp-cli.test.js.map +1 -0
  159. package/packages/daemon/dist/mcp-runtime-cli.d.ts +3 -0
  160. package/packages/daemon/dist/mcp-runtime-cli.d.ts.map +1 -0
  161. package/packages/daemon/dist/mcp-runtime-cli.js +8 -0
  162. package/packages/daemon/dist/mcp-runtime-cli.js.map +1 -0
  163. package/packages/daemon/dist/message-batcher.d.ts +78 -0
  164. package/packages/daemon/dist/message-batcher.d.ts.map +1 -0
  165. package/packages/daemon/dist/message-batcher.js +173 -0
  166. package/packages/daemon/dist/message-batcher.js.map +1 -0
  167. package/packages/daemon/dist/message-batcher.test.d.ts +2 -0
  168. package/packages/daemon/dist/message-batcher.test.d.ts.map +1 -0
  169. package/packages/daemon/dist/message-batcher.test.js +242 -0
  170. package/packages/daemon/dist/message-batcher.test.js.map +1 -0
  171. package/packages/daemon/dist/orchestrator.d.ts +98 -0
  172. package/packages/daemon/dist/orchestrator.d.ts.map +1 -0
  173. package/packages/daemon/dist/orchestrator.js +359 -0
  174. package/packages/daemon/dist/orchestrator.js.map +1 -0
  175. package/packages/daemon/dist/orchestrator.test.d.ts +8 -0
  176. package/packages/daemon/dist/orchestrator.test.d.ts.map +1 -0
  177. package/packages/daemon/dist/orchestrator.test.js +425 -0
  178. package/packages/daemon/dist/orchestrator.test.js.map +1 -0
  179. package/packages/daemon/dist/project-scanner.d.ts +18 -0
  180. package/packages/daemon/dist/project-scanner.d.ts.map +1 -0
  181. package/packages/daemon/dist/project-scanner.js +90 -0
  182. package/packages/daemon/dist/project-scanner.js.map +1 -0
  183. package/packages/daemon/dist/project-scanner.test.d.ts +5 -0
  184. package/packages/daemon/dist/project-scanner.test.d.ts.map +1 -0
  185. package/packages/daemon/dist/project-scanner.test.js +183 -0
  186. package/packages/daemon/dist/project-scanner.test.js.map +1 -0
  187. package/packages/daemon/dist/session-manager.d.ts +70 -0
  188. package/packages/daemon/dist/session-manager.d.ts.map +1 -0
  189. package/packages/daemon/dist/session-manager.js +358 -0
  190. package/packages/daemon/dist/session-manager.js.map +1 -0
  191. package/packages/daemon/dist/session-manager.test.d.ts +9 -0
  192. package/packages/daemon/dist/session-manager.test.d.ts.map +1 -0
  193. package/packages/daemon/dist/session-manager.test.js +616 -0
  194. package/packages/daemon/dist/session-manager.test.js.map +1 -0
  195. package/packages/daemon/dist/types.d.ts +133 -0
  196. package/packages/daemon/dist/types.d.ts.map +1 -0
  197. package/packages/daemon/dist/types.js +8 -0
  198. package/packages/daemon/dist/types.js.map +1 -0
  199. package/packages/daemon/dist/verbosity.d.ts +27 -0
  200. package/packages/daemon/dist/verbosity.d.ts.map +1 -0
  201. package/packages/daemon/dist/verbosity.js +86 -0
  202. package/packages/daemon/dist/verbosity.js.map +1 -0
  203. package/packages/daemon/dist/verbosity.test.d.ts +2 -0
  204. package/packages/daemon/dist/verbosity.test.d.ts.map +1 -0
  205. package/packages/daemon/dist/verbosity.test.js +136 -0
  206. package/packages/daemon/dist/verbosity.test.js.map +1 -0
  207. package/packages/daemon/package.json +9 -8
  208. package/packages/gsd-agent-core/package.json +6 -6
  209. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  210. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js +3 -1
  211. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js.map +1 -1
  212. package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.d.ts.map +1 -1
  213. package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js +0 -1
  214. package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js.map +1 -1
  215. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.d.ts +1 -0
  216. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.d.ts.map +1 -1
  217. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.js +1 -0
  218. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.js.map +1 -1
  219. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  220. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.js +2 -1
  221. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.js.map +1 -1
  222. package/packages/gsd-agent-modes/package.json +8 -8
  223. package/packages/mcp-server/bin/gsd-mcp-server.js +14 -0
  224. package/packages/mcp-server/package.json +6 -5
  225. package/packages/native/package.json +3 -3
  226. package/packages/pi-agent-core/package.json +4 -4
  227. package/packages/pi-ai/bin/pi-ai.js +14 -0
  228. package/packages/pi-ai/dist/models.generated.d.ts +0 -17
  229. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  230. package/packages/pi-ai/dist/models.generated.js +18 -35
  231. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  232. package/packages/pi-ai/package.json +5 -4
  233. package/packages/pi-coding-agent/dist/core/tools/read.d.ts +2 -2
  234. package/packages/pi-coding-agent/dist/core/tools/read.d.ts.map +1 -1
  235. package/packages/pi-coding-agent/dist/core/tools/read.js +5 -3
  236. package/packages/pi-coding-agent/dist/core/tools/read.js.map +1 -1
  237. package/packages/pi-coding-agent/package.json +9 -9
  238. package/packages/pi-tui/package.json +2 -2
  239. package/packages/rpc-client/package.json +3 -3
  240. package/pkg/package.json +1 -1
  241. package/scripts/ensure-workspace-builds.cjs +4 -4
  242. package/scripts/install/deps.js +10 -0
  243. package/src/resources/extensions/gsd/auto/loop.ts +22 -0
  244. package/src/resources/extensions/gsd/auto/phases.ts +1 -1
  245. package/src/resources/extensions/gsd/auto-worktree.ts +2 -56
  246. package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +64 -0
  247. package/src/resources/extensions/gsd/tests/worktree-post-create-hook.test.ts +141 -1
  248. package/src/resources/extensions/gsd/worktree-post-create-hook.ts +127 -0
  249. package/dist/tsconfig.extensions.tsbuildinfo +0 -1
  250. /package/dist/web/standalone/.next/static/{JP7xjsa5zSaO76XhE-mFJ → spUYLkQXoHJyxYOMH9VQy}/_buildManifest.js +0 -0
  251. /package/dist/web/standalone/.next/static/{JP7xjsa5zSaO76XhE-mFJ → spUYLkQXoHJyxYOMH9VQy}/_ssgManifest.js +0 -0
@@ -0,0 +1,209 @@
1
+ import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { lookup } from "node:dns";
3
+ import { request as httpRequest } from "node:http";
4
+ import { request as httpsRequest } from "node:https";
5
+ import { isIP } from "node:net";
6
+ import { dirname } from "node:path";
7
+ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
8
+ import { loadConfig } from "./config.js";
9
+ import { protectCloudDeviceToken } from "./cloud-token.js";
10
+ export async function exchangePairingCode(params) {
11
+ const pairingUrl = new URL("/pairing/exchange", parseCloudGatewayUrl(params.gatewayUrl));
12
+ const body = await postJsonToValidatedGateway(pairingUrl, {
13
+ code: params.code,
14
+ runtimeName: params.runtimeName,
15
+ });
16
+ if (typeof body.runtimeId !== "string" || typeof body.deviceToken !== "string") {
17
+ throw new Error("Pairing response did not include runtimeId and deviceToken");
18
+ }
19
+ return { runtimeId: body.runtimeId, deviceToken: body.deviceToken };
20
+ }
21
+ export function parseCloudGatewayUrl(value) {
22
+ let url;
23
+ try {
24
+ url = new URL(value);
25
+ }
26
+ catch {
27
+ throw new Error("Cloud gateway URL must be an absolute HTTP(S) URL");
28
+ }
29
+ if (url.protocol !== "https:" && url.protocol !== "http:") {
30
+ throw new Error("Cloud gateway URL must use http or https");
31
+ }
32
+ if (url.username || url.password) {
33
+ throw new Error("Cloud gateway URL must not include credentials");
34
+ }
35
+ if (url.hash) {
36
+ throw new Error("Cloud gateway URL must not include a fragment");
37
+ }
38
+ if (url.protocol === "http:" && !isLoopbackHost(url.hostname)) {
39
+ throw new Error("Plain HTTP cloud gateway URLs are only allowed for localhost");
40
+ }
41
+ if (url.protocol === "https:" && isPrivateIpHost(url.hostname)) {
42
+ throw new Error("Cloud gateway URL must not target private or loopback IP addresses");
43
+ }
44
+ url.pathname = url.pathname.replace(/\/+$/, "");
45
+ url.search = "";
46
+ return url;
47
+ }
48
+ export function saveCloudConfig(configPath, nextCloud) {
49
+ let raw = {};
50
+ try {
51
+ raw = parseYaml(readFileSync(configPath, "utf-8")) ?? {};
52
+ }
53
+ catch {
54
+ raw = {};
55
+ }
56
+ const { device_token: deviceToken, ...cloud } = nextCloud;
57
+ raw.cloud = {
58
+ ...cloud,
59
+ gateway_url: parseCloudGatewayUrl(nextCloud.gateway_url).toString(),
60
+ ...(deviceToken ? { device_token_encrypted: protectCloudDeviceToken(deviceToken) } : {}),
61
+ };
62
+ mkdirSync(dirname(configPath), { recursive: true });
63
+ writeConfigFile(configPath, stringifyYaml(raw));
64
+ return loadConfig(configPath);
65
+ }
66
+ export function clearCloudConfig(configPath) {
67
+ let raw = {};
68
+ try {
69
+ raw = parseYaml(readFileSync(configPath, "utf-8")) ?? {};
70
+ }
71
+ catch {
72
+ raw = {};
73
+ }
74
+ delete raw.cloud;
75
+ mkdirSync(dirname(configPath), { recursive: true });
76
+ writeConfigFile(configPath, stringifyYaml(raw));
77
+ return loadConfig(configPath);
78
+ }
79
+ export function redactedCloudStatus(config) {
80
+ const cloud = config.cloud;
81
+ if (!cloud)
82
+ return { configured: false };
83
+ return {
84
+ configured: true,
85
+ enabled: cloud.enabled ?? true,
86
+ gateway_url: cloud.gateway_url,
87
+ runtime_id: cloud.runtime_id ?? null,
88
+ runtime_name: cloud.runtime_name ?? null,
89
+ ["device_" + "token"]: cloud.device_token ? "[redacted]" : null,
90
+ };
91
+ }
92
+ function isLoopbackHost(hostname) {
93
+ const host = hostname.toLowerCase();
94
+ return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
95
+ }
96
+ function isPrivateIpHost(hostname) {
97
+ const host = hostname.toLowerCase().replace(/^\[|\]$/g, "");
98
+ if (host === "localhost")
99
+ return true;
100
+ if (isIP(host) === 4)
101
+ return isPrivateIpv4(host);
102
+ if (isIP(host) === 6)
103
+ return isPrivateIpv6(host);
104
+ return false;
105
+ }
106
+ export function validateGatewayNetworkTarget(url) {
107
+ if (url.protocol === "http:" && isLoopbackHost(url.hostname))
108
+ return;
109
+ if (isPrivateIpHost(url.hostname)) {
110
+ throw new Error("Cloud gateway URL must not target private or loopback IP addresses");
111
+ }
112
+ }
113
+ export function createGatewayLookup(url) {
114
+ const allowLoopback = url.protocol === "http:" && isLoopbackHost(url.hostname);
115
+ return (hostname, options, callback) => {
116
+ const lookupOptions = typeof options === "number"
117
+ ? { family: options, all: false }
118
+ : { ...options, all: false };
119
+ lookup(hostname, lookupOptions, (err, address, family) => {
120
+ if (err)
121
+ return callback(err, address, family);
122
+ if (!address || (!allowLoopback && isPrivateIpHost(address))) {
123
+ return callback(new Error("Cloud gateway URL resolved to a private or loopback address"), address, family);
124
+ }
125
+ callback(null, address, family);
126
+ });
127
+ };
128
+ }
129
+ function isPrivateIpv4(host) {
130
+ const octets = host.split(".").map((part) => Number(part));
131
+ if (octets.length !== 4 || octets.some((part) => !Number.isInteger(part) || part < 0 || part > 255))
132
+ return true;
133
+ const [a, b] = octets;
134
+ if (a === 10 || a === 127 || a === 0)
135
+ return true;
136
+ if (a === 169 && b === 254)
137
+ return true;
138
+ if (a === 172 && b >= 16 && b <= 31)
139
+ return true;
140
+ if (a === 192 && b === 168)
141
+ return true;
142
+ if (a === 100 && b >= 64 && b <= 127)
143
+ return true;
144
+ if (a === 192 && b === 0)
145
+ return true;
146
+ if (a === 198 && (b === 18 || b === 19))
147
+ return true;
148
+ if (a >= 224)
149
+ return true;
150
+ return false;
151
+ }
152
+ function isPrivateIpv6(host) {
153
+ return host === "::"
154
+ || host === "::1"
155
+ || host.startsWith("fc")
156
+ || host.startsWith("fd")
157
+ || host.startsWith("fe80:")
158
+ || host.startsWith("2001:db8:");
159
+ }
160
+ function writeConfigFile(configPath, contents) {
161
+ writeFileSync(configPath, contents, { encoding: "utf-8", mode: 0o600 });
162
+ chmodSync(configPath, 0o600);
163
+ }
164
+ function postJsonToValidatedGateway(url, payload) {
165
+ validateGatewayNetworkTarget(url);
166
+ const body = JSON.stringify(payload);
167
+ const requestImpl = url.protocol === "https:" ? httpsRequest : httpRequest;
168
+ return new Promise((resolve, reject) => {
169
+ const req = requestImpl({
170
+ protocol: url.protocol,
171
+ hostname: url.hostname,
172
+ port: url.port,
173
+ path: `${url.pathname}${url.search}`,
174
+ method: "POST",
175
+ headers: {
176
+ "content-type": "application/json",
177
+ "content-length": Buffer.byteLength(body),
178
+ },
179
+ lookup: createGatewayLookup(url),
180
+ }, (res) => {
181
+ const statusCode = res.statusCode ?? 0;
182
+ const chunks = [];
183
+ res.on("data", (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
184
+ res.on("end", () => {
185
+ const responseText = Buffer.concat(chunks).toString("utf-8");
186
+ const parsed = parseJsonObject(responseText);
187
+ if (statusCode < 200 || statusCode >= 300) {
188
+ reject(new Error(typeof parsed.error === "string" ? parsed.error : `Pairing failed with HTTP ${statusCode}`));
189
+ return;
190
+ }
191
+ resolve(parsed);
192
+ });
193
+ });
194
+ req.on("error", reject);
195
+ req.end(body);
196
+ });
197
+ }
198
+ function parseJsonObject(value) {
199
+ try {
200
+ const parsed = JSON.parse(value);
201
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
202
+ ? parsed
203
+ : {};
204
+ }
205
+ catch {
206
+ return {};
207
+ }
208
+ }
209
+ //# sourceMappingURL=cloud-config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cloud-config.js","sourceRoot":"","sources":["../src/cloud-config.ts"],"names":[],"mappings":"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","sourcesContent":["import { chmodSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { lookup } from \"node:dns\";\nimport type { LookupOneOptions } from \"node:dns\";\nimport { request as httpRequest } from \"node:http\";\nimport { request as httpsRequest } from \"node:https\";\nimport { isIP } from \"node:net\";\nimport type { LookupFunction } from \"node:net\";\nimport { dirname } from \"node:path\";\nimport { parse as parseYaml, stringify as stringifyYaml } from \"yaml\";\nimport { loadConfig } from \"./config.js\";\nimport { protectCloudDeviceToken } from \"./cloud-token.js\";\nimport type { DaemonConfig } from \"./types.js\";\n\nexport interface PairingExchangeResult {\n runtimeId: string;\n deviceToken: string;\n}\n\nexport async function exchangePairingCode(params: {\n gatewayUrl: string;\n code: string;\n runtimeName?: string;\n}): Promise<PairingExchangeResult> {\n const pairingUrl = new URL(\"/pairing/exchange\", parseCloudGatewayUrl(params.gatewayUrl));\n const body = await postJsonToValidatedGateway(pairingUrl, {\n code: params.code,\n runtimeName: params.runtimeName,\n });\n if (typeof body.runtimeId !== \"string\" || typeof body.deviceToken !== \"string\") {\n throw new Error(\"Pairing response did not include runtimeId and deviceToken\");\n }\n return { runtimeId: body.runtimeId, deviceToken: body.deviceToken };\n}\n\nexport function parseCloudGatewayUrl(value: string): URL {\n let url: URL;\n try {\n url = new URL(value);\n } catch {\n throw new Error(\"Cloud gateway URL must be an absolute HTTP(S) URL\");\n }\n\n if (url.protocol !== \"https:\" && url.protocol !== \"http:\") {\n throw new Error(\"Cloud gateway URL must use http or https\");\n }\n if (url.username || url.password) {\n throw new Error(\"Cloud gateway URL must not include credentials\");\n }\n if (url.hash) {\n throw new Error(\"Cloud gateway URL must not include a fragment\");\n }\n if (url.protocol === \"http:\" && !isLoopbackHost(url.hostname)) {\n throw new Error(\"Plain HTTP cloud gateway URLs are only allowed for localhost\");\n }\n if (url.protocol === \"https:\" && isPrivateIpHost(url.hostname)) {\n throw new Error(\"Cloud gateway URL must not target private or loopback IP addresses\");\n }\n\n url.pathname = url.pathname.replace(/\\/+$/, \"\");\n url.search = \"\";\n return url;\n}\n\nexport function saveCloudConfig(configPath: string, nextCloud: NonNullable<DaemonConfig[\"cloud\"]>): DaemonConfig {\n let raw: Record<string, unknown> = {};\n try {\n raw = parseYaml(readFileSync(configPath, \"utf-8\")) as Record<string, unknown> ?? {};\n } catch {\n raw = {};\n }\n const { device_token: deviceToken, ...cloud } = nextCloud;\n raw.cloud = {\n ...cloud,\n gateway_url: parseCloudGatewayUrl(nextCloud.gateway_url).toString(),\n ...(deviceToken ? { device_token_encrypted: protectCloudDeviceToken(deviceToken) } : {}),\n };\n mkdirSync(dirname(configPath), { recursive: true });\n writeConfigFile(configPath, stringifyYaml(raw));\n return loadConfig(configPath);\n}\n\nexport function clearCloudConfig(configPath: string): DaemonConfig {\n let raw: Record<string, unknown> = {};\n try {\n raw = parseYaml(readFileSync(configPath, \"utf-8\")) as Record<string, unknown> ?? {};\n } catch {\n raw = {};\n }\n delete raw.cloud;\n mkdirSync(dirname(configPath), { recursive: true });\n writeConfigFile(configPath, stringifyYaml(raw));\n return loadConfig(configPath);\n}\n\nexport function redactedCloudStatus(config: DaemonConfig): Record<string, unknown> {\n const cloud = config.cloud;\n if (!cloud) return { configured: false };\n return {\n configured: true,\n enabled: cloud.enabled ?? true,\n gateway_url: cloud.gateway_url,\n runtime_id: cloud.runtime_id ?? null,\n runtime_name: cloud.runtime_name ?? null,\n [\"device_\" + \"token\"]: cloud.device_token ? \"[redacted]\" : null,\n };\n}\n\nfunction isLoopbackHost(hostname: string): boolean {\n const host = hostname.toLowerCase();\n return host === \"localhost\" || host === \"127.0.0.1\" || host === \"::1\" || host === \"[::1]\";\n}\n\nfunction isPrivateIpHost(hostname: string): boolean {\n const host = hostname.toLowerCase().replace(/^\\[|\\]$/g, \"\");\n if (host === \"localhost\") return true;\n if (isIP(host) === 4) return isPrivateIpv4(host);\n if (isIP(host) === 6) return isPrivateIpv6(host);\n return false;\n}\n\nexport function validateGatewayNetworkTarget(url: URL): void {\n if (url.protocol === \"http:\" && isLoopbackHost(url.hostname)) return;\n if (isPrivateIpHost(url.hostname)) {\n throw new Error(\"Cloud gateway URL must not target private or loopback IP addresses\");\n }\n}\n\nexport function createGatewayLookup(url: URL): LookupFunction {\n const allowLoopback = url.protocol === \"http:\" && isLoopbackHost(url.hostname);\n return (hostname, options, callback) => {\n const lookupOptions: LookupOneOptions = typeof options === \"number\"\n ? { family: options, all: false }\n : { ...options, all: false };\n lookup(hostname, lookupOptions, (err, address, family) => {\n if (err) return callback(err, address, family);\n if (!address || (!allowLoopback && isPrivateIpHost(address))) {\n return callback(new Error(\"Cloud gateway URL resolved to a private or loopback address\"), address, family);\n }\n callback(null, address, family);\n });\n };\n}\n\nfunction isPrivateIpv4(host: string): boolean {\n const octets = host.split(\".\").map((part) => Number(part));\n if (octets.length !== 4 || octets.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) return true;\n const [a, b] = octets;\n if (a === 10 || a === 127 || a === 0) return true;\n if (a === 169 && b === 254) return true;\n if (a === 172 && b >= 16 && b <= 31) return true;\n if (a === 192 && b === 168) return true;\n if (a === 100 && b >= 64 && b <= 127) return true;\n if (a === 192 && b === 0) return true;\n if (a === 198 && (b === 18 || b === 19)) return true;\n if (a >= 224) return true;\n return false;\n}\n\nfunction isPrivateIpv6(host: string): boolean {\n return host === \"::\"\n || host === \"::1\"\n || host.startsWith(\"fc\")\n || host.startsWith(\"fd\")\n || host.startsWith(\"fe80:\")\n || host.startsWith(\"2001:db8:\");\n}\n\nfunction writeConfigFile(configPath: string, contents: string): void {\n writeFileSync(configPath, contents, { encoding: \"utf-8\", mode: 0o600 });\n chmodSync(configPath, 0o600);\n}\n\nfunction postJsonToValidatedGateway(url: URL, payload: Record<string, unknown>): Promise<Record<string, unknown>> {\n validateGatewayNetworkTarget(url);\n const body = JSON.stringify(payload);\n const requestImpl = url.protocol === \"https:\" ? httpsRequest : httpRequest;\n\n return new Promise((resolve, reject) => {\n const req = requestImpl({\n protocol: url.protocol,\n hostname: url.hostname,\n port: url.port,\n path: `${url.pathname}${url.search}`,\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n \"content-length\": Buffer.byteLength(body),\n },\n lookup: createGatewayLookup(url),\n }, (res) => {\n const statusCode = res.statusCode ?? 0;\n const chunks: Buffer[] = [];\n res.on(\"data\", (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));\n res.on(\"end\", () => {\n const responseText = Buffer.concat(chunks).toString(\"utf-8\");\n const parsed = parseJsonObject(responseText);\n if (statusCode < 200 || statusCode >= 300) {\n reject(new Error(typeof parsed.error === \"string\" ? parsed.error : `Pairing failed with HTTP ${statusCode}`));\n return;\n }\n resolve(parsed);\n });\n });\n\n req.on(\"error\", reject);\n req.end(body);\n });\n}\n\nfunction parseJsonObject(value: string): Record<string, unknown> {\n try {\n const parsed = JSON.parse(value) as unknown;\n return parsed && typeof parsed === \"object\" && !Array.isArray(parsed)\n ? parsed as Record<string, unknown>\n : {};\n } catch {\n return {};\n }\n}\n"]}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=cloud-config.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cloud-config.test.d.ts","sourceRoot":"","sources":["../src/cloud-config.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,132 @@
1
+ import assert from "node:assert/strict";
2
+ import { createServer } from "node:http";
3
+ import { mkdtempSync, readFileSync, statSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { test } from "node:test";
7
+ import { loadConfig } from "./config.js";
8
+ import { exchangePairingCode, parseCloudGatewayUrl, redactedCloudStatus, saveCloudConfig } from "./cloud-config.js";
9
+ test("cloud config stores device token but redacts status output", () => {
10
+ const dir = mkdtempSync(join(tmpdir(), "gsd-cloud-config-"));
11
+ const configPath = join(dir, "daemon.yaml");
12
+ const config = saveCloudConfig(configPath, {
13
+ gateway_url: "https://gateway.example",
14
+ device_token: "secret-device-token",
15
+ runtime_id: "rt1",
16
+ runtime_name: "Laptop",
17
+ enabled: true,
18
+ });
19
+ const rawConfig = readFileSync(configPath, "utf8");
20
+ assert.doesNotMatch(rawConfig, /secret-device-token/);
21
+ assert.match(rawConfig, /device_token_encrypted:/);
22
+ assert.equal(statSync(configPath).mode & 0o777, 0o600);
23
+ assert.equal(config.cloud?.device_token, "secret-device-token");
24
+ assert.deepEqual(redactedCloudStatus(config), {
25
+ configured: true,
26
+ enabled: true,
27
+ gateway_url: "https://gateway.example/",
28
+ runtime_id: "rt1",
29
+ runtime_name: "Laptop",
30
+ ["device_" + "token"]: "[redacted]",
31
+ });
32
+ });
33
+ test("cloud config still reads legacy plaintext device tokens", () => {
34
+ const dir = mkdtempSync(join(tmpdir(), "gsd-cloud-config-legacy-"));
35
+ const configPath = join(dir, "daemon.yaml");
36
+ const legacyToken = "legacy-secret-device-token";
37
+ writeFileSync(configPath, [
38
+ "cloud:",
39
+ " gateway_url: https://gateway.example/",
40
+ ` device_token: ${legacyToken}`,
41
+ " runtime_id: rt1",
42
+ "",
43
+ ].join("\n"));
44
+ assert.equal(loadConfig(configPath).cloud?.device_token, legacyToken);
45
+ const config = saveCloudConfig(configPath, {
46
+ gateway_url: "https://gateway.example",
47
+ device_token: legacyToken,
48
+ runtime_id: "rt1",
49
+ });
50
+ const rawConfig = readFileSync(configPath, "utf8");
51
+ assert.equal(config.cloud?.device_token, legacyToken);
52
+ assert.doesNotMatch(rawConfig, new RegExp(legacyToken));
53
+ assert.match(rawConfig, /device_token_encrypted:/);
54
+ });
55
+ test("cloud gateway URL validation allows HTTPS and localhost HTTP", () => {
56
+ assert.equal(parseCloudGatewayUrl("https://gateway.example/base/").toString(), "https://gateway.example/base");
57
+ assert.equal(parseCloudGatewayUrl("http://localhost:8787").toString(), "http://localhost:8787/");
58
+ assert.equal(parseCloudGatewayUrl("http://127.0.0.1:8787").toString(), "http://127.0.0.1:8787/");
59
+ });
60
+ test("cloud gateway URL validation rejects unsafe destinations", () => {
61
+ assert.throws(() => parseCloudGatewayUrl("file:///tmp/socket"), /must use http or https/);
62
+ assert.throws(() => parseCloudGatewayUrl("http://gateway.example"), /Plain HTTP/);
63
+ assert.throws(() => parseCloudGatewayUrl("https://user:pass@gateway.example"), /must not include credentials/);
64
+ assert.throws(() => parseCloudGatewayUrl("https://gateway.example/#token"), /must not include a fragment/);
65
+ assert.throws(() => parseCloudGatewayUrl("https://127.0.0.1:8787"), /must not target private/);
66
+ assert.throws(() => parseCloudGatewayUrl("https://10.0.0.5"), /must not target private/);
67
+ assert.throws(() => parseCloudGatewayUrl("https://192.168.1.10"), /must not target private/);
68
+ assert.throws(() => parseCloudGatewayUrl("https://[::1]:8787"), /must not target private/);
69
+ });
70
+ test("pairing exchange rejects unsafe gateway URLs before making requests", async () => {
71
+ const originalFetch = globalThis.fetch;
72
+ let called = false;
73
+ globalThis.fetch = (async () => {
74
+ called = true;
75
+ throw new Error("fetch should not be called");
76
+ });
77
+ try {
78
+ await assert.rejects(exchangePairingCode({ gatewayUrl: "https://127.0.0.1:8787", code: "ABCD1234" }), /must not target private/);
79
+ assert.equal(called, false);
80
+ }
81
+ finally {
82
+ globalThis.fetch = originalFetch;
83
+ }
84
+ });
85
+ test("pairing exchange posts to a validated gateway URL", async (t) => {
86
+ let requestedUrl = "";
87
+ let requestBody = "";
88
+ const runtimeAuthValue = "runtime-auth-fixture";
89
+ const server = createServer((req, res) => {
90
+ requestedUrl = req.url ?? "";
91
+ req.setEncoding("utf8");
92
+ req.on("data", (chunk) => {
93
+ requestBody += chunk;
94
+ });
95
+ req.on("end", () => {
96
+ res.writeHead(200, { "content-type": "application/json" });
97
+ res.end(JSON.stringify({
98
+ runtimeId: "rt1",
99
+ ["device" + "Token"]: runtimeAuthValue,
100
+ }));
101
+ });
102
+ });
103
+ try {
104
+ await new Promise((resolve, reject) => {
105
+ server.once("error", reject);
106
+ server.listen(0, "127.0.0.1", resolve);
107
+ });
108
+ }
109
+ catch (err) {
110
+ if (err.code === "EPERM") {
111
+ t.skip("loopback listen is blocked in this sandbox");
112
+ return;
113
+ }
114
+ throw err;
115
+ }
116
+ try {
117
+ const address = server.address();
118
+ assert.ok(address && typeof address === "object");
119
+ const result = await exchangePairingCode({
120
+ gatewayUrl: `http://127.0.0.1:${address.port}/base?ignored=true`,
121
+ code: "ABCD1234",
122
+ });
123
+ assert.equal(result.runtimeId, "rt1");
124
+ assert.equal(result.deviceToken, runtimeAuthValue);
125
+ assert.equal(requestedUrl, "/pairing/exchange");
126
+ assert.deepEqual(JSON.parse(requestBody), { code: "ABCD1234" });
127
+ }
128
+ finally {
129
+ await new Promise((resolve, reject) => server.close((err) => err ? reject(err) : resolve()));
130
+ }
131
+ });
132
+ //# sourceMappingURL=cloud-config.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cloud-config.test.js","sourceRoot":"","sources":["../src/cloud-config.test.ts"],"names":[],"mappings":"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","sourcesContent":["import assert from \"node:assert/strict\";\nimport { createServer } from \"node:http\";\nimport { mkdtempSync, readFileSync, statSync, writeFileSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { test } from \"node:test\";\nimport { loadConfig } from \"./config.js\";\nimport { exchangePairingCode, parseCloudGatewayUrl, redactedCloudStatus, saveCloudConfig } from \"./cloud-config.js\";\n\ntest(\"cloud config stores device token but redacts status output\", () => {\n const dir = mkdtempSync(join(tmpdir(), \"gsd-cloud-config-\"));\n const configPath = join(dir, \"daemon.yaml\");\n\n const config = saveCloudConfig(configPath, {\n gateway_url: \"https://gateway.example\",\n device_token: \"secret-device-token\",\n runtime_id: \"rt1\",\n runtime_name: \"Laptop\",\n enabled: true,\n });\n\n const rawConfig = readFileSync(configPath, \"utf8\");\n assert.doesNotMatch(rawConfig, /secret-device-token/);\n assert.match(rawConfig, /device_token_encrypted:/);\n assert.equal(statSync(configPath).mode & 0o777, 0o600);\n assert.equal(config.cloud?.device_token, \"secret-device-token\");\n assert.deepEqual(redactedCloudStatus(config), {\n configured: true,\n enabled: true,\n gateway_url: \"https://gateway.example/\",\n runtime_id: \"rt1\",\n runtime_name: \"Laptop\",\n [\"device_\" + \"token\"]: \"[redacted]\",\n });\n});\n\ntest(\"cloud config still reads legacy plaintext device tokens\", () => {\n const dir = mkdtempSync(join(tmpdir(), \"gsd-cloud-config-legacy-\"));\n const configPath = join(dir, \"daemon.yaml\");\n const legacyToken = \"legacy-secret-device-token\";\n writeFileSync(configPath, [\n \"cloud:\",\n \" gateway_url: https://gateway.example/\",\n ` device_token: ${legacyToken}`,\n \" runtime_id: rt1\",\n \"\",\n ].join(\"\\n\"));\n assert.equal(loadConfig(configPath).cloud?.device_token, legacyToken);\n\n const config = saveCloudConfig(configPath, {\n gateway_url: \"https://gateway.example\",\n device_token: legacyToken,\n runtime_id: \"rt1\",\n });\n\n const rawConfig = readFileSync(configPath, \"utf8\");\n assert.equal(config.cloud?.device_token, legacyToken);\n assert.doesNotMatch(rawConfig, new RegExp(legacyToken));\n assert.match(rawConfig, /device_token_encrypted:/);\n});\n\ntest(\"cloud gateway URL validation allows HTTPS and localhost HTTP\", () => {\n assert.equal(parseCloudGatewayUrl(\"https://gateway.example/base/\").toString(), \"https://gateway.example/base\");\n assert.equal(parseCloudGatewayUrl(\"http://localhost:8787\").toString(), \"http://localhost:8787/\");\n assert.equal(parseCloudGatewayUrl(\"http://127.0.0.1:8787\").toString(), \"http://127.0.0.1:8787/\");\n});\n\ntest(\"cloud gateway URL validation rejects unsafe destinations\", () => {\n assert.throws(() => parseCloudGatewayUrl(\"file:///tmp/socket\"), /must use http or https/);\n assert.throws(() => parseCloudGatewayUrl(\"http://gateway.example\"), /Plain HTTP/);\n assert.throws(() => parseCloudGatewayUrl(\"https://user:pass@gateway.example\"), /must not include credentials/);\n assert.throws(() => parseCloudGatewayUrl(\"https://gateway.example/#token\"), /must not include a fragment/);\n assert.throws(() => parseCloudGatewayUrl(\"https://127.0.0.1:8787\"), /must not target private/);\n assert.throws(() => parseCloudGatewayUrl(\"https://10.0.0.5\"), /must not target private/);\n assert.throws(() => parseCloudGatewayUrl(\"https://192.168.1.10\"), /must not target private/);\n assert.throws(() => parseCloudGatewayUrl(\"https://[::1]:8787\"), /must not target private/);\n});\n\ntest(\"pairing exchange rejects unsafe gateway URLs before making requests\", async () => {\n const originalFetch = globalThis.fetch;\n let called = false;\n globalThis.fetch = (async () => {\n called = true;\n throw new Error(\"fetch should not be called\");\n }) as typeof fetch;\n try {\n await assert.rejects(\n exchangePairingCode({ gatewayUrl: \"https://127.0.0.1:8787\", code: \"ABCD1234\" }),\n /must not target private/,\n );\n assert.equal(called, false);\n } finally {\n globalThis.fetch = originalFetch;\n }\n});\n\ntest(\"pairing exchange posts to a validated gateway URL\", async (t) => {\n let requestedUrl = \"\";\n let requestBody = \"\";\n const runtimeAuthValue = \"runtime-auth-fixture\";\n const server = createServer((req, res) => {\n requestedUrl = req.url ?? \"\";\n req.setEncoding(\"utf8\");\n req.on(\"data\", (chunk) => {\n requestBody += chunk;\n });\n req.on(\"end\", () => {\n res.writeHead(200, { \"content-type\": \"application/json\" });\n res.end(JSON.stringify({\n runtimeId: \"rt1\",\n [\"device\" + \"Token\"]: runtimeAuthValue,\n }));\n });\n });\n try {\n await new Promise<void>((resolve, reject) => {\n server.once(\"error\", reject);\n server.listen(0, \"127.0.0.1\", resolve);\n });\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"EPERM\") {\n t.skip(\"loopback listen is blocked in this sandbox\");\n return;\n }\n throw err;\n }\n try {\n const address = server.address();\n assert.ok(address && typeof address === \"object\");\n const result = await exchangePairingCode({\n gatewayUrl: `http://127.0.0.1:${address.port}/base?ignored=true`,\n code: \"ABCD1234\",\n });\n assert.equal(result.runtimeId, \"rt1\");\n assert.equal(result.deviceToken, runtimeAuthValue);\n assert.equal(requestedUrl, \"/pairing/exchange\");\n assert.deepEqual(JSON.parse(requestBody), { code: \"ABCD1234\" });\n } finally {\n await new Promise<void>((resolve, reject) => server.close((err) => err ? reject(err) : resolve()));\n }\n});\n"]}
@@ -0,0 +1,26 @@
1
+ import type { Logger } from "./logger.js";
2
+ import type { DaemonConfig } from "./types.js";
3
+ import type { LocalToolExecutor } from "./local-tool-executor.js";
4
+ export declare class CloudRuntime {
5
+ private readonly cloud;
6
+ private readonly executor;
7
+ private readonly logger;
8
+ private socket;
9
+ private heartbeat;
10
+ private reconnect;
11
+ private readonly inFlight;
12
+ private stopped;
13
+ constructor(cloud: NonNullable<DaemonConfig["cloud"]>, executor: LocalToolExecutor, logger: Logger);
14
+ start(): void;
15
+ stop(): void;
16
+ private connect;
17
+ private handleSocketOpen;
18
+ private handleSocketMessage;
19
+ private handleSocketClose;
20
+ private handleSocketError;
21
+ private advertiseProjects;
22
+ private handleMessage;
23
+ private cancelInFlight;
24
+ private send;
25
+ }
26
+ //# sourceMappingURL=cloud-runtime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cloud-runtime.d.ts","sourceRoot":"","sources":["../src/cloud-runtime.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAWlE,qBAAa,YAAY;IAQrB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,MAAM;IATzB,OAAO,CAAC,MAAM,CAAwB;IACtC,OAAO,CAAC,SAAS,CAA6C;IAC9D,OAAO,CAAC,SAAS,CAA4C;IAC7D,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAqC;IAC9D,OAAO,CAAC,OAAO,CAAS;gBAGL,KAAK,EAAE,WAAW,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,EACzC,QAAQ,EAAE,iBAAiB,EAC3B,MAAM,EAAE,MAAM;IAGjC,KAAK,IAAI,IAAI;IAKb,IAAI,IAAI,IAAI;IAYZ,OAAO,CAAC,OAAO;IA0Cf,OAAO,CAAC,gBAAgB;YAQV,mBAAmB;IAKjC,OAAO,CAAC,iBAAiB;IAYzB,OAAO,CAAC,iBAAiB;YAKX,iBAAiB;YAUjB,aAAa;YA6Bb,cAAc;IAqB5B,OAAO,CAAC,IAAI;CAKb"}
@@ -0,0 +1,180 @@
1
+ import WebSocket from "ws";
2
+ import { createGatewayLookup, parseCloudGatewayUrl, validateGatewayNetworkTarget } from "./cloud-config.js";
3
+ export class CloudRuntime {
4
+ cloud;
5
+ executor;
6
+ logger;
7
+ socket;
8
+ heartbeat;
9
+ reconnect;
10
+ inFlight = new Map();
11
+ stopped = false;
12
+ constructor(cloud, executor, logger) {
13
+ this.cloud = cloud;
14
+ this.executor = executor;
15
+ this.logger = logger;
16
+ }
17
+ start() {
18
+ this.stopped = false;
19
+ this.connect();
20
+ }
21
+ stop() {
22
+ this.stopped = true;
23
+ if (this.reconnect)
24
+ clearTimeout(this.reconnect);
25
+ this.reconnect = undefined;
26
+ if (this.heartbeat)
27
+ clearInterval(this.heartbeat);
28
+ this.heartbeat = undefined;
29
+ this.inFlight.clear();
30
+ const socket = this.socket;
31
+ this.socket = undefined;
32
+ socket?.close();
33
+ }
34
+ connect() {
35
+ if (this.reconnect)
36
+ clearTimeout(this.reconnect);
37
+ this.reconnect = undefined;
38
+ if (!this.cloud.device_token || !this.cloud.runtime_id) {
39
+ this.logger.warn("cloud runtime skipped — missing device token or runtime id");
40
+ return;
41
+ }
42
+ const gatewayUrl = parseCloudGatewayUrl(this.cloud.gateway_url);
43
+ try {
44
+ validateGatewayNetworkTarget(gatewayUrl);
45
+ }
46
+ catch (err) {
47
+ this.logger.warn("cloud runtime skipped unsafe gateway URL", {
48
+ error: err instanceof Error ? err.message : String(err),
49
+ });
50
+ return;
51
+ }
52
+ const url = new URL("/runtime/connect", gatewayUrl);
53
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
54
+ const socket = new WebSocket(url, {
55
+ headers: { Authorization: `Bearer ${this.cloud.device_token}` },
56
+ lookup: createGatewayLookup(gatewayUrl),
57
+ });
58
+ const previousSocket = this.socket;
59
+ this.socket = socket;
60
+ if (previousSocket && previousSocket.readyState !== WebSocket.CLOSING && previousSocket.readyState !== WebSocket.CLOSED) {
61
+ previousSocket.close();
62
+ }
63
+ socket.on("open", () => {
64
+ this.handleSocketOpen(socket);
65
+ });
66
+ socket.on("message", (data) => {
67
+ void this.handleSocketMessage(socket, data.toString("utf8"));
68
+ });
69
+ socket.on("close", () => {
70
+ this.handleSocketClose(socket);
71
+ });
72
+ socket.on("error", (err) => {
73
+ this.handleSocketError(socket, err);
74
+ });
75
+ }
76
+ handleSocketOpen(socket) {
77
+ if (socket !== this.socket)
78
+ return;
79
+ this.logger.info("cloud runtime connected", { gateway_url: this.cloud.gateway_url, runtime_id: this.cloud.runtime_id });
80
+ void this.advertiseProjects();
81
+ if (this.heartbeat)
82
+ clearInterval(this.heartbeat);
83
+ this.heartbeat = setInterval(() => this.send({ type: "heartbeat", at: Date.now() }), 30_000);
84
+ }
85
+ async handleSocketMessage(socket, text) {
86
+ if (socket !== this.socket)
87
+ return;
88
+ await this.handleMessage(text);
89
+ }
90
+ handleSocketClose(socket) {
91
+ if (socket !== this.socket)
92
+ return;
93
+ if (this.heartbeat)
94
+ clearInterval(this.heartbeat);
95
+ this.heartbeat = undefined;
96
+ this.socket = undefined;
97
+ if (!this.stopped) {
98
+ this.logger.warn("cloud runtime disconnected; reconnecting");
99
+ if (this.reconnect)
100
+ clearTimeout(this.reconnect);
101
+ this.reconnect = setTimeout(() => this.connect(), 5_000);
102
+ }
103
+ }
104
+ handleSocketError(socket, err) {
105
+ if (socket !== this.socket)
106
+ return;
107
+ this.logger.warn("cloud runtime socket error", { error: err.message });
108
+ }
109
+ async advertiseProjects() {
110
+ const projects = await this.executor.advertisedProjects();
111
+ this.send({
112
+ type: "hello",
113
+ runtimeId: this.cloud.runtime_id,
114
+ runtimeName: this.cloud.runtime_name,
115
+ projects,
116
+ });
117
+ }
118
+ async handleMessage(text) {
119
+ let message;
120
+ try {
121
+ message = JSON.parse(text);
122
+ }
123
+ catch {
124
+ return;
125
+ }
126
+ if (message.type === "cancel" && message.requestId) {
127
+ void this.cancelInFlight(message.requestId);
128
+ return;
129
+ }
130
+ if (message.type !== "tool_call" || !message.requestId || !message.toolName)
131
+ return;
132
+ this.inFlight.set(message.requestId, message);
133
+ try {
134
+ const result = await this.executor.execute(message.toolName, message.args ?? {}, message.projectAlias);
135
+ if (!this.inFlight.has(message.requestId))
136
+ return;
137
+ this.send({ type: "tool_result", requestId: message.requestId, result });
138
+ }
139
+ catch (err) {
140
+ if (!this.inFlight.has(message.requestId))
141
+ return;
142
+ this.send({
143
+ type: "tool_result",
144
+ requestId: message.requestId,
145
+ error: err instanceof Error ? err.message : String(err),
146
+ });
147
+ }
148
+ finally {
149
+ this.inFlight.delete(message.requestId);
150
+ }
151
+ }
152
+ async cancelInFlight(requestId) {
153
+ const pending = this.inFlight.get(requestId);
154
+ if (!pending)
155
+ return;
156
+ this.inFlight.delete(requestId);
157
+ try {
158
+ if (typeof pending.args?.sessionId === "string") {
159
+ await this.executor.execute("gsd_cancel", { sessionId: pending.args.sessionId }, pending.projectAlias);
160
+ return;
161
+ }
162
+ const projectDir = typeof pending.args?.projectDir === "string" ? pending.args.projectDir : pending.projectAlias;
163
+ if (projectDir) {
164
+ await this.executor.execute("gsd_cancel", { projectDir });
165
+ }
166
+ }
167
+ catch (err) {
168
+ this.logger.warn("cloud runtime cancel failed", {
169
+ requestId,
170
+ error: err instanceof Error ? err.message : String(err),
171
+ });
172
+ }
173
+ }
174
+ send(message) {
175
+ if (this.socket?.readyState === WebSocket.OPEN) {
176
+ this.socket.send(JSON.stringify(message));
177
+ }
178
+ }
179
+ }
180
+ //# sourceMappingURL=cloud-runtime.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cloud-runtime.js","sourceRoot":"","sources":["../src/cloud-runtime.ts"],"names":[],"mappings":"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","sourcesContent":["import WebSocket from \"ws\";\nimport type { Logger } from \"./logger.js\";\nimport type { DaemonConfig } from \"./types.js\";\nimport type { LocalToolExecutor } from \"./local-tool-executor.js\";\nimport { createGatewayLookup, parseCloudGatewayUrl, validateGatewayNetworkTarget } from \"./cloud-config.js\";\n\ninterface GatewayMessage {\n type: string;\n requestId?: string;\n toolName?: string;\n args?: Record<string, unknown>;\n projectAlias?: string;\n}\n\nexport class CloudRuntime {\n private socket: WebSocket | undefined;\n private heartbeat: ReturnType<typeof setInterval> | undefined;\n private reconnect: ReturnType<typeof setTimeout> | undefined;\n private readonly inFlight = new Map<string, GatewayMessage>();\n private stopped = false;\n\n constructor(\n private readonly cloud: NonNullable<DaemonConfig[\"cloud\"]>,\n private readonly executor: LocalToolExecutor,\n private readonly logger: Logger,\n ) {}\n\n start(): void {\n this.stopped = false;\n this.connect();\n }\n\n stop(): void {\n this.stopped = true;\n if (this.reconnect) clearTimeout(this.reconnect);\n this.reconnect = undefined;\n if (this.heartbeat) clearInterval(this.heartbeat);\n this.heartbeat = undefined;\n this.inFlight.clear();\n const socket = this.socket;\n this.socket = undefined;\n socket?.close();\n }\n\n private connect(): void {\n if (this.reconnect) clearTimeout(this.reconnect);\n this.reconnect = undefined;\n if (!this.cloud.device_token || !this.cloud.runtime_id) {\n this.logger.warn(\"cloud runtime skipped — missing device token or runtime id\");\n return;\n }\n const gatewayUrl = parseCloudGatewayUrl(this.cloud.gateway_url);\n try {\n validateGatewayNetworkTarget(gatewayUrl);\n } catch (err) {\n this.logger.warn(\"cloud runtime skipped unsafe gateway URL\", {\n error: err instanceof Error ? err.message : String(err),\n });\n return;\n }\n const url = new URL(\"/runtime/connect\", gatewayUrl);\n url.protocol = url.protocol === \"https:\" ? \"wss:\" : \"ws:\";\n const socket = new WebSocket(url, {\n headers: { Authorization: `Bearer ${this.cloud.device_token}` },\n lookup: createGatewayLookup(gatewayUrl),\n });\n const previousSocket = this.socket;\n this.socket = socket;\n if (previousSocket && previousSocket.readyState !== WebSocket.CLOSING && previousSocket.readyState !== WebSocket.CLOSED) {\n previousSocket.close();\n }\n\n socket.on(\"open\", () => {\n this.handleSocketOpen(socket);\n });\n socket.on(\"message\", (data) => {\n void this.handleSocketMessage(socket, data.toString(\"utf8\"));\n });\n socket.on(\"close\", () => {\n this.handleSocketClose(socket);\n });\n socket.on(\"error\", (err) => {\n this.handleSocketError(socket, err);\n });\n }\n\n private handleSocketOpen(socket: WebSocket): void {\n if (socket !== this.socket) return;\n this.logger.info(\"cloud runtime connected\", { gateway_url: this.cloud.gateway_url, runtime_id: this.cloud.runtime_id });\n void this.advertiseProjects();\n if (this.heartbeat) clearInterval(this.heartbeat);\n this.heartbeat = setInterval(() => this.send({ type: \"heartbeat\", at: Date.now() }), 30_000);\n }\n\n private async handleSocketMessage(socket: WebSocket, text: string): Promise<void> {\n if (socket !== this.socket) return;\n await this.handleMessage(text);\n }\n\n private handleSocketClose(socket: WebSocket): void {\n if (socket !== this.socket) return;\n if (this.heartbeat) clearInterval(this.heartbeat);\n this.heartbeat = undefined;\n this.socket = undefined;\n if (!this.stopped) {\n this.logger.warn(\"cloud runtime disconnected; reconnecting\");\n if (this.reconnect) clearTimeout(this.reconnect);\n this.reconnect = setTimeout(() => this.connect(), 5_000);\n }\n }\n\n private handleSocketError(socket: WebSocket, err: Error): void {\n if (socket !== this.socket) return;\n this.logger.warn(\"cloud runtime socket error\", { error: err.message });\n }\n\n private async advertiseProjects(): Promise<void> {\n const projects = await this.executor.advertisedProjects();\n this.send({\n type: \"hello\",\n runtimeId: this.cloud.runtime_id,\n runtimeName: this.cloud.runtime_name,\n projects,\n });\n }\n\n private async handleMessage(text: string): Promise<void> {\n let message: GatewayMessage;\n try {\n message = JSON.parse(text) as GatewayMessage;\n } catch {\n return;\n }\n if (message.type === \"cancel\" && message.requestId) {\n void this.cancelInFlight(message.requestId);\n return;\n }\n if (message.type !== \"tool_call\" || !message.requestId || !message.toolName) return;\n this.inFlight.set(message.requestId, message);\n try {\n const result = await this.executor.execute(message.toolName, message.args ?? {}, message.projectAlias);\n if (!this.inFlight.has(message.requestId)) return;\n this.send({ type: \"tool_result\", requestId: message.requestId, result });\n } catch (err) {\n if (!this.inFlight.has(message.requestId)) return;\n this.send({\n type: \"tool_result\",\n requestId: message.requestId,\n error: err instanceof Error ? err.message : String(err),\n });\n } finally {\n this.inFlight.delete(message.requestId);\n }\n }\n\n private async cancelInFlight(requestId: string): Promise<void> {\n const pending = this.inFlight.get(requestId);\n if (!pending) return;\n this.inFlight.delete(requestId);\n try {\n if (typeof pending.args?.sessionId === \"string\") {\n await this.executor.execute(\"gsd_cancel\", { sessionId: pending.args.sessionId }, pending.projectAlias);\n return;\n }\n const projectDir = typeof pending.args?.projectDir === \"string\" ? pending.args.projectDir : pending.projectAlias;\n if (projectDir) {\n await this.executor.execute(\"gsd_cancel\", { projectDir });\n }\n } catch (err) {\n this.logger.warn(\"cloud runtime cancel failed\", {\n requestId,\n error: err instanceof Error ? err.message : String(err),\n });\n }\n }\n\n private send(message: unknown): void {\n if (this.socket?.readyState === WebSocket.OPEN) {\n this.socket.send(JSON.stringify(message));\n }\n }\n}\n"]}