@opengsd/gsd-pi 1.6.0 → 1.7.0-dev.30c4ec2f

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 (229) hide show
  1. package/README.md +6 -10
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/extensions/gsd/auto-post-unit.js +19 -3
  4. package/dist/resources/extensions/gsd/auto-prompts.js +20 -0
  5. package/dist/resources/extensions/gsd/auto.js +12 -0
  6. package/dist/resources/extensions/gsd/browser-daemon-auto-prep.js +68 -2
  7. package/dist/resources/extensions/gsd/doctor-history.js +65 -0
  8. package/dist/resources/extensions/gsd/doctor-state-checks.js +509 -0
  9. package/dist/resources/extensions/gsd/doctor-workspace-checks.js +91 -0
  10. package/dist/resources/extensions/gsd/doctor.js +14 -653
  11. package/dist/resources/extensions/gsd/md-importer.js +7 -2
  12. package/dist/resources/extensions/gsd/migrate/planning-writer.js +8 -2
  13. package/dist/resources/extensions/gsd/migration-auto-check.js +4 -0
  14. package/dist/resources/extensions/gsd/reopen-reason.js +98 -0
  15. package/dist/resources/extensions/gsd/session-status-io.js +31 -0
  16. package/dist/resources/extensions/gsd/state-reconciliation/drift/project-md.js +54 -14
  17. package/dist/resources/extensions/gsd/state-reconciliation/drift/sketch-flag.js +48 -5
  18. package/dist/resources/extensions/gsd/tools/reopen-task.js +15 -0
  19. package/dist/resources/extensions/shared/gsd-browser-cli.js +22 -5
  20. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  21. package/dist/web/standalone/.next/BUILD_ID +1 -1
  22. package/dist/web/standalone/.next/app-path-routes-manifest.json +10 -10
  23. package/dist/web/standalone/.next/build-manifest.json +3 -3
  24. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  25. package/dist/web/standalone/.next/required-server-files.json +3 -3
  26. package/dist/web/standalone/.next/server/app/_global-error/page.js +3 -3
  27. package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  28. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  29. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/_not-found/page.js +2 -2
  37. package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  38. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  39. package/dist/web/standalone/.next/server/app/_not-found.rsc +3 -3
  40. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
  41. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +3 -3
  43. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
  47. package/dist/web/standalone/.next/server/app/api/boot/route_client-reference-manifest.js +1 -1
  48. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js +1 -1
  49. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route_client-reference-manifest.js +1 -1
  50. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js +1 -1
  51. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route_client-reference-manifest.js +1 -1
  52. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js +2 -2
  53. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route_client-reference-manifest.js +1 -1
  54. package/dist/web/standalone/.next/server/app/api/browse-directories/route.js +1 -1
  55. package/dist/web/standalone/.next/server/app/api/browse-directories/route_client-reference-manifest.js +1 -1
  56. package/dist/web/standalone/.next/server/app/api/captures/route.js +1 -1
  57. package/dist/web/standalone/.next/server/app/api/captures/route_client-reference-manifest.js +1 -1
  58. package/dist/web/standalone/.next/server/app/api/cleanup/route.js +1 -1
  59. package/dist/web/standalone/.next/server/app/api/cleanup/route_client-reference-manifest.js +1 -1
  60. package/dist/web/standalone/.next/server/app/api/dev-mode/route.js +1 -1
  61. package/dist/web/standalone/.next/server/app/api/dev-mode/route_client-reference-manifest.js +1 -1
  62. package/dist/web/standalone/.next/server/app/api/doctor/route.js +1 -1
  63. package/dist/web/standalone/.next/server/app/api/doctor/route_client-reference-manifest.js +1 -1
  64. package/dist/web/standalone/.next/server/app/api/experimental/route.js +2 -2
  65. package/dist/web/standalone/.next/server/app/api/experimental/route_client-reference-manifest.js +1 -1
  66. package/dist/web/standalone/.next/server/app/api/export-data/route.js +1 -1
  67. package/dist/web/standalone/.next/server/app/api/export-data/route_client-reference-manifest.js +1 -1
  68. package/dist/web/standalone/.next/server/app/api/files/route.js +1 -1
  69. package/dist/web/standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
  70. package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
  71. package/dist/web/standalone/.next/server/app/api/forensics/route_client-reference-manifest.js +1 -1
  72. package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
  73. package/dist/web/standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
  74. package/dist/web/standalone/.next/server/app/api/history/route.js +1 -1
  75. package/dist/web/standalone/.next/server/app/api/history/route_client-reference-manifest.js +1 -1
  76. package/dist/web/standalone/.next/server/app/api/hooks/route.js +1 -1
  77. package/dist/web/standalone/.next/server/app/api/hooks/route_client-reference-manifest.js +1 -1
  78. package/dist/web/standalone/.next/server/app/api/inspect/route.js +1 -1
  79. package/dist/web/standalone/.next/server/app/api/inspect/route_client-reference-manifest.js +1 -1
  80. package/dist/web/standalone/.next/server/app/api/knowledge/route.js +1 -1
  81. package/dist/web/standalone/.next/server/app/api/knowledge/route_client-reference-manifest.js +1 -1
  82. package/dist/web/standalone/.next/server/app/api/live-state/route.js +1 -1
  83. package/dist/web/standalone/.next/server/app/api/live-state/route_client-reference-manifest.js +1 -1
  84. package/dist/web/standalone/.next/server/app/api/mcp-connections/route.js +1 -1
  85. package/dist/web/standalone/.next/server/app/api/mcp-connections/route_client-reference-manifest.js +1 -1
  86. package/dist/web/standalone/.next/server/app/api/notifications/route.js +2 -2
  87. package/dist/web/standalone/.next/server/app/api/notifications/route_client-reference-manifest.js +1 -1
  88. package/dist/web/standalone/.next/server/app/api/onboarding/route.js +1 -1
  89. package/dist/web/standalone/.next/server/app/api/onboarding/route_client-reference-manifest.js +1 -1
  90. package/dist/web/standalone/.next/server/app/api/preferences/route.js +1 -1
  91. package/dist/web/standalone/.next/server/app/api/preferences/route_client-reference-manifest.js +1 -1
  92. package/dist/web/standalone/.next/server/app/api/projects/route.js +1 -1
  93. package/dist/web/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
  94. package/dist/web/standalone/.next/server/app/api/recovery/route.js +1 -1
  95. package/dist/web/standalone/.next/server/app/api/recovery/route_client-reference-manifest.js +1 -1
  96. package/dist/web/standalone/.next/server/app/api/remote-questions/route.js +2 -2
  97. package/dist/web/standalone/.next/server/app/api/remote-questions/route_client-reference-manifest.js +1 -1
  98. package/dist/web/standalone/.next/server/app/api/session/browser/route.js +1 -1
  99. package/dist/web/standalone/.next/server/app/api/session/browser/route_client-reference-manifest.js +1 -1
  100. package/dist/web/standalone/.next/server/app/api/session/command/route.js +1 -1
  101. package/dist/web/standalone/.next/server/app/api/session/command/route_client-reference-manifest.js +1 -1
  102. package/dist/web/standalone/.next/server/app/api/session/events/route.js +2 -2
  103. package/dist/web/standalone/.next/server/app/api/session/events/route_client-reference-manifest.js +1 -1
  104. package/dist/web/standalone/.next/server/app/api/session/manage/route.js +1 -1
  105. package/dist/web/standalone/.next/server/app/api/session/manage/route_client-reference-manifest.js +1 -1
  106. package/dist/web/standalone/.next/server/app/api/settings-data/route.js +1 -1
  107. package/dist/web/standalone/.next/server/app/api/settings-data/route_client-reference-manifest.js +1 -1
  108. package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
  109. package/dist/web/standalone/.next/server/app/api/shutdown/route_client-reference-manifest.js +1 -1
  110. package/dist/web/standalone/.next/server/app/api/skill-health/route.js +1 -1
  111. package/dist/web/standalone/.next/server/app/api/skill-health/route_client-reference-manifest.js +1 -1
  112. package/dist/web/standalone/.next/server/app/api/steer/route.js +1 -1
  113. package/dist/web/standalone/.next/server/app/api/steer/route_client-reference-manifest.js +1 -1
  114. package/dist/web/standalone/.next/server/app/api/switch-root/route.js +1 -1
  115. package/dist/web/standalone/.next/server/app/api/switch-root/route_client-reference-manifest.js +1 -1
  116. package/dist/web/standalone/.next/server/app/api/terminal/input/route.js +1 -1
  117. package/dist/web/standalone/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -1
  118. package/dist/web/standalone/.next/server/app/api/terminal/resize/route.js +2 -2
  119. package/dist/web/standalone/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -1
  120. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js +1 -1
  121. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route_client-reference-manifest.js +1 -1
  122. package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js +4 -4
  123. package/dist/web/standalone/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -1
  124. package/dist/web/standalone/.next/server/app/api/terminal/upload/route.js +1 -1
  125. package/dist/web/standalone/.next/server/app/api/terminal/upload/route_client-reference-manifest.js +1 -1
  126. package/dist/web/standalone/.next/server/app/api/undo/route.js +1 -1
  127. package/dist/web/standalone/.next/server/app/api/undo/route_client-reference-manifest.js +1 -1
  128. package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
  129. package/dist/web/standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
  130. package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
  131. package/dist/web/standalone/.next/server/app/api/visualizer/route_client-reference-manifest.js +1 -1
  132. package/dist/web/standalone/.next/server/app/index.html +1 -1
  133. package/dist/web/standalone/.next/server/app/index.rsc +4 -4
  134. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  135. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +4 -4
  136. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  137. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +3 -3
  138. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  139. package/dist/web/standalone/.next/server/app/page.js +2 -2
  140. package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  141. package/dist/web/standalone/.next/server/app-paths-manifest.json +10 -10
  142. package/dist/web/standalone/.next/server/chunks/2842.js +4 -4
  143. package/dist/web/standalone/.next/server/chunks/8357.js +2 -2
  144. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  145. package/dist/web/standalone/.next/server/middleware.js +3 -3
  146. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  147. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  148. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  149. package/dist/web/standalone/.next/static/chunks/app/_not-found/page-a6fb1847f67f167c.js +1 -0
  150. package/dist/web/standalone/.next/static/chunks/app/{layout-b35cbfff38aaf4cf.js → layout-4ae2d68984392bbf.js} +1 -1
  151. package/dist/web/standalone/.next/static/chunks/app/page-72a856634ad14c10.js +1 -0
  152. package/dist/web/standalone/.next/static/chunks/{main-app-590a74400e35f685.js → main-app-90d1d8d5e5d2dc6b.js} +1 -1
  153. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-1115d46ac61b9823.js +1 -0
  154. package/dist/web/standalone/node_modules/@gsd/native/package.json +1 -1
  155. package/dist/web/standalone/node_modules/node-pty/build/Makefile +2 -2
  156. package/dist/web/standalone/node_modules/node-pty/build/Release/pty.node +0 -0
  157. package/dist/web/standalone/node_modules/node-pty/build/pty.target.mk +14 -14
  158. package/dist/web/standalone/server.js +1 -1
  159. package/integrations/hermes/open_gsd_hermes/gsd_client.py +1 -1
  160. package/integrations/hermes/pyproject.toml +1 -1
  161. package/package.json +6 -6
  162. package/packages/cloud-mcp-gateway/package.json +2 -2
  163. package/packages/contracts/package.json +1 -1
  164. package/packages/daemon/dist/cloud-cli.d.ts +1 -1
  165. package/packages/daemon/dist/cloud-cli.d.ts.map +1 -1
  166. package/packages/daemon/dist/cloud-cli.js +42 -2
  167. package/packages/daemon/dist/cloud-cli.js.map +1 -1
  168. package/packages/daemon/dist/cloud-config.d.ts +1 -0
  169. package/packages/daemon/dist/cloud-config.d.ts.map +1 -1
  170. package/packages/daemon/dist/cloud-config.js +26 -8
  171. package/packages/daemon/dist/cloud-config.js.map +1 -1
  172. package/packages/daemon/dist/cloud-config.test.js +19 -1
  173. package/packages/daemon/dist/cloud-config.test.js.map +1 -1
  174. package/packages/daemon/dist/device-flow.d.ts +24 -0
  175. package/packages/daemon/dist/device-flow.d.ts.map +1 -0
  176. package/packages/daemon/dist/device-flow.js +128 -0
  177. package/packages/daemon/dist/device-flow.js.map +1 -0
  178. package/packages/daemon/dist/device-flow.test.d.ts +2 -0
  179. package/packages/daemon/dist/device-flow.test.d.ts.map +1 -0
  180. package/packages/daemon/dist/device-flow.test.js +172 -0
  181. package/packages/daemon/dist/device-flow.test.js.map +1 -0
  182. package/packages/daemon/package.json +5 -5
  183. package/packages/gsd-agent-core/package.json +5 -5
  184. package/packages/gsd-agent-modes/package.json +7 -7
  185. package/packages/gsd-cloud/README.md +48 -0
  186. package/packages/gsd-cloud/bin/gsd-cloud.js +28 -0
  187. package/packages/gsd-cloud/package.json +40 -0
  188. package/packages/mcp-server/package.json +4 -4
  189. package/packages/native/package.json +1 -1
  190. package/packages/pi-agent-core/package.json +1 -1
  191. package/packages/pi-ai/package.json +1 -1
  192. package/packages/pi-coding-agent/package.json +7 -7
  193. package/packages/pi-tui/package.json +2 -2
  194. package/packages/rpc-client/package.json +2 -2
  195. package/pkg/package.json +1 -1
  196. package/src/resources/extensions/browser-tools/tests/gsd-browser-launch-config.test.mjs +19 -0
  197. package/src/resources/extensions/gsd/auto-post-unit.ts +22 -3
  198. package/src/resources/extensions/gsd/auto-prompts.ts +20 -0
  199. package/src/resources/extensions/gsd/auto.ts +12 -0
  200. package/src/resources/extensions/gsd/browser-daemon-auto-prep.ts +77 -1
  201. package/src/resources/extensions/gsd/doctor-history.ts +81 -0
  202. package/src/resources/extensions/gsd/doctor-state-checks.ts +527 -0
  203. package/src/resources/extensions/gsd/doctor-workspace-checks.ts +97 -0
  204. package/src/resources/extensions/gsd/doctor.ts +14 -673
  205. package/src/resources/extensions/gsd/md-importer.ts +7 -2
  206. package/src/resources/extensions/gsd/migrate/planning-writer.ts +9 -3
  207. package/src/resources/extensions/gsd/migration-auto-check.ts +3 -0
  208. package/src/resources/extensions/gsd/reopen-reason.ts +109 -0
  209. package/src/resources/extensions/gsd/session-status-io.ts +34 -0
  210. package/src/resources/extensions/gsd/state-reconciliation/drift/project-md.ts +67 -20
  211. package/src/resources/extensions/gsd/state-reconciliation/drift/sketch-flag.ts +47 -5
  212. package/src/resources/extensions/gsd/tests/browser-daemon-auto-prep.test.ts +26 -0
  213. package/src/resources/extensions/gsd/tests/db-authority-regression.test.ts +76 -1
  214. package/src/resources/extensions/gsd/tests/doctor-history-public-api.test.ts +25 -0
  215. package/src/resources/extensions/gsd/tests/migrate-hierarchy.test.ts +53 -0
  216. package/src/resources/extensions/gsd/tests/migration-auto-check.test.ts +53 -0
  217. package/src/resources/extensions/gsd/tests/parsers-legacy-importers.test.ts +4 -0
  218. package/src/resources/extensions/gsd/tests/planning-writer.test.ts +18 -1
  219. package/src/resources/extensions/gsd/tests/progressive-planning.test.ts +18 -3
  220. package/src/resources/extensions/gsd/tests/reopen-reason.test.ts +117 -0
  221. package/src/resources/extensions/gsd/tests/state-reconciliation-drift.test.ts +177 -25
  222. package/src/resources/extensions/gsd/tests/worker-resume-wait.test.ts +73 -0
  223. package/src/resources/extensions/gsd/tools/reopen-task.ts +15 -0
  224. package/src/resources/extensions/shared/gsd-browser-cli.ts +35 -7
  225. package/dist/web/standalone/.next/static/chunks/app/_not-found/page-49f565245e1e4afe.js +0 -1
  226. package/dist/web/standalone/.next/static/chunks/app/page-bc4c723c6a19c6f0.js +0 -1
  227. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-ec3eaffc9785ba48.js +0 -1
  228. /package/dist/web/standalone/.next/static/{6xYMOzxVhsQgryneJ5KRc → ue2JDvA1wboxPBPeblD5N}/_buildManifest.js +0 -0
  229. /package/dist/web/standalone/.next/static/{6xYMOzxVhsQgryneJ5KRc → ue2JDvA1wboxPBPeblD5N}/_ssgManifest.js +0 -0
@@ -1 +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"]}
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 { LookupOptions } 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 const rejected = (address: string): boolean =>\n !address || (!allowLoopback && isPrivateIpHost(address));\n // Node's socket connect (autoSelectFamily / Happy Eyeballs, default-on since\n // Node 20) calls a custom lookup with `all: true` and expects an ARRAY\n // callback. Forcing `all: false` + a scalar callback made every real gateway\n // request throw \"Invalid IP address: undefined\". Honor both forms; the SSRF\n // guard is applied to every resolved address (reject the whole lookup if any\n // is private/loopback — an attacker-controlled resolver must not slip one in).\n return ((hostname: string, options: unknown, callback: (...args: unknown[]) => void) => {\n const wantsAll =\n typeof options === \"object\" && options !== null && (options as LookupOptions).all === true;\n const base: LookupOptions =\n typeof options === \"number\" ? { family: options } : { ...(options as LookupOptions) };\n if (wantsAll) {\n lookup(hostname, { ...base, all: true }, (err, addresses) => {\n if (err) return callback(err);\n const list = addresses as Array<{ address: string; family: number }>;\n if (!list.length || list.some((entry) => rejected(entry.address))) {\n return callback(new Error(\"Cloud gateway URL resolved to a private or loopback address\"));\n }\n callback(null, list);\n });\n return;\n }\n lookup(hostname, { ...base, all: false }, (err, address, family) => {\n if (err) return callback(err, address, family);\n if (rejected(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 }) as unknown as LookupFunction;\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\nexport function 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"]}
@@ -5,7 +5,25 @@ import { tmpdir } from "node:os";
5
5
  import { join } from "node:path";
6
6
  import { test } from "node:test";
7
7
  import { loadConfig } from "./config.js";
8
- import { exchangePairingCode, parseCloudGatewayUrl, redactedCloudStatus, saveCloudConfig } from "./cloud-config.js";
8
+ import { createGatewayLookup, exchangePairingCode, parseCloudGatewayUrl, redactedCloudStatus, saveCloudConfig } from "./cloud-config.js";
9
+ // Invoke a gateway lookup and capture (err, result) from its Node-style callback.
10
+ function runGatewayLookup(url, options) {
11
+ const lookup = createGatewayLookup(new URL(url));
12
+ return new Promise((resolve) => {
13
+ lookup("localhost", options, (...args) => resolve({ err: args[0] ?? null, result: args[1] }));
14
+ });
15
+ }
16
+ test("createGatewayLookup honors all:true with an array callback (regression: scalar form threw 'Invalid IP address' on Node >=20)", async () => {
17
+ const { err, result } = await runGatewayLookup("http://localhost", { all: true, family: 0 });
18
+ assert.equal(err, null, "loopback allowed for http URL");
19
+ assert.ok(Array.isArray(result), "all:true must call back with an array");
20
+ assert.ok(result.length > 0, "expected at least one address");
21
+ });
22
+ test("createGatewayLookup all:true still enforces the SSRF guard on every address", async () => {
23
+ const { err } = await runGatewayLookup("https://localhost", { all: true, family: 0 });
24
+ assert.ok(err instanceof Error, "https loopback must be rejected");
25
+ assert.match(err.message, /private or loopback/);
26
+ });
9
27
  test("cloud config stores device token but redacts status output", () => {
10
28
  const dir = mkdtempSync(join(tmpdir(), "gsd-cloud-config-"));
11
29
  const configPath = join(dir, "daemon.yaml");
@@ -1 +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"]}
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 { createGatewayLookup, exchangePairingCode, parseCloudGatewayUrl, redactedCloudStatus, saveCloudConfig } from \"./cloud-config.js\";\n\n// Invoke a gateway lookup and capture (err, result) from its Node-style callback.\nfunction runGatewayLookup(url: string, options: unknown): Promise<{ err: Error | null; result: unknown }> {\n const lookup = createGatewayLookup(new URL(url));\n return new Promise((resolve) => {\n (lookup as unknown as (h: string, o: unknown, cb: (...a: unknown[]) => void) => void)(\n \"localhost\",\n options,\n (...args: unknown[]) => resolve({ err: (args[0] as Error | null) ?? null, result: args[1] }),\n );\n });\n}\n\ntest(\"createGatewayLookup honors all:true with an array callback (regression: scalar form threw 'Invalid IP address' on Node >=20)\", async () => {\n const { err, result } = await runGatewayLookup(\"http://localhost\", { all: true, family: 0 });\n assert.equal(err, null, \"loopback allowed for http URL\");\n assert.ok(Array.isArray(result), \"all:true must call back with an array\");\n assert.ok((result as unknown[]).length > 0, \"expected at least one address\");\n});\n\ntest(\"createGatewayLookup all:true still enforces the SSRF guard on every address\", async () => {\n const { err } = await runGatewayLookup(\"https://localhost\", { all: true, family: 0 });\n assert.ok(err instanceof Error, \"https loopback must be rejected\");\n assert.match(err!.message, /private or loopback/);\n});\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,24 @@
1
+ export interface DeviceFlowResult {
2
+ deviceToken: string;
3
+ runtimeId: string;
4
+ /**
5
+ * Resolved relay/gateway URL: the server-returned `gateway_url` from the approved
6
+ * token-poll response when present AND valid, else the `gatewayUrl` the caller passed
7
+ * (D-02 backward-compat — existing single-host `--gateway` users are unaffected).
8
+ */
9
+ gatewayUrl: string;
10
+ }
11
+ export interface DeviceFlowParams {
12
+ gatewayUrl: string;
13
+ configPath: string;
14
+ runtimeName?: string;
15
+ binaryName: string;
16
+ }
17
+ /**
18
+ * Run the full RFC 8628 device authorization flow.
19
+ * Requests a device code, displays the verification URL and user code in the terminal,
20
+ * polls for approval with a spinner, and returns the device token on success.
21
+ * Calls process.exit(1) on denial (D-12) or timeout (D-11).
22
+ */
23
+ export declare function runDeviceFlow(params: DeviceFlowParams): Promise<DeviceFlowResult>;
24
+ //# sourceMappingURL=device-flow.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"device-flow.d.ts","sourceRoot":"","sources":["../src/device-flow.ts"],"names":[],"mappings":"AAUA,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;CACpB;AAKD;;;;;GAKG;AACH,wBAAsB,aAAa,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CA+HvF"}
@@ -0,0 +1,128 @@
1
+ // Device flow handler: code request, terminal display, polling loop, config save.
2
+ // Implements RFC 8628 device authorization grant for the daemon's `gsd cloud login` command.
3
+ import { hostname as osHostname } from "node:os";
4
+ import { cursorTo, clearLine } from "node:readline";
5
+ import { parseCloudGatewayUrl, postJsonToValidatedGateway, validateGatewayNetworkTarget } from "./cloud-config.js";
6
+ const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
7
+ const DEFAULT_POLL_INTERVAL_MS = 5_000;
8
+ const SLOW_DOWN_INCREMENT_MS = 5_000;
9
+ /** Simple sleep helper. */
10
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
11
+ /**
12
+ * Run the full RFC 8628 device authorization flow.
13
+ * Requests a device code, displays the verification URL and user code in the terminal,
14
+ * polls for approval with a spinner, and returns the device token on success.
15
+ * Calls process.exit(1) on denial (D-12) or timeout (D-11).
16
+ */
17
+ export async function runDeviceFlow(params) {
18
+ const base = parseCloudGatewayUrl(params.gatewayUrl);
19
+ // ---- Step 1: Request device code (D-01, DEVF-01) ----
20
+ const codeUrl = new URL(base.pathname.replace(/\/+$/, "") + "/api/device/code", base);
21
+ const codeResponse = await postJsonToValidatedGateway(codeUrl, {
22
+ hostname: osHostname(),
23
+ os: process.platform,
24
+ ...(params.runtimeName !== undefined ? { runtimeName: params.runtimeName } : {}),
25
+ });
26
+ const userCode = typeof codeResponse["userCode"] === "string" ? codeResponse["userCode"] : "";
27
+ const deviceCode = typeof codeResponse["deviceCode"] === "string" ? codeResponse["deviceCode"] : "";
28
+ const verificationUriComplete = typeof codeResponse["verificationUriComplete"] === "string"
29
+ ? codeResponse["verificationUriComplete"]
30
+ : "";
31
+ const expiresIn = typeof codeResponse["expiresIn"] === "number" ? codeResponse["expiresIn"] : 600;
32
+ if (!userCode || !deviceCode) {
33
+ throw new Error("Device code response missing userCode or deviceCode");
34
+ }
35
+ // ---- Step 2: Display to user (D-02) ----
36
+ process.stdout.write(`\n${params.binaryName}: Cloud Login\n`);
37
+ process.stdout.write(`\nTo authorize this machine, open the following URL in your browser:\n`);
38
+ process.stdout.write(`\n ${verificationUriComplete}\n`);
39
+ process.stdout.write(`\nOr visit the gateway and enter the code manually:\n`);
40
+ process.stdout.write(`\n ${base.origin}\n`);
41
+ process.stdout.write(`\nYour code: ${userCode}\n\n`);
42
+ // ---- Step 3: Polling loop (D-10, D-11, D-12) ----
43
+ const tokenUrl = new URL(base.pathname.replace(/\/+$/, "") + "/api/device/token", base);
44
+ const expiresAt = Date.now() + expiresIn * 1_000;
45
+ let pollIntervalMs = DEFAULT_POLL_INTERVAL_MS;
46
+ let spinnerIndex = 0;
47
+ const spinnerInterval = setInterval(() => {
48
+ const frame = SPINNER_FRAMES[spinnerIndex % SPINNER_FRAMES.length] ?? "⠋";
49
+ spinnerIndex++;
50
+ const remaining = Math.max(0, Math.ceil((expiresAt - Date.now()) / 1_000));
51
+ cursorTo(process.stdout, 0);
52
+ clearLine(process.stdout, 0);
53
+ process.stdout.write(` ${frame} Waiting for approval... (expires in ${remaining}s)`);
54
+ }, 100);
55
+ try {
56
+ while (Date.now() < expiresAt) {
57
+ await sleep(pollIntervalMs);
58
+ if (Date.now() >= expiresAt)
59
+ break;
60
+ let tokenResponse;
61
+ try {
62
+ tokenResponse = await postJsonToValidatedGateway(tokenUrl, { deviceCode });
63
+ }
64
+ catch {
65
+ // Network errors during polling are transient — keep trying until expiry.
66
+ continue;
67
+ }
68
+ const status = typeof tokenResponse["status"] === "string" ? tokenResponse["status"] : "";
69
+ if (status === "approved") {
70
+ clearInterval(spinnerInterval);
71
+ cursorTo(process.stdout, 0);
72
+ clearLine(process.stdout, 0);
73
+ process.stdout.write(`${params.binaryName}: Authorization approved!\n`);
74
+ const deviceToken = typeof tokenResponse["token"] === "string" ? tokenResponse["token"] : "";
75
+ const runtimeId = typeof tokenResponse["runtimeId"] === "string" ? tokenResponse["runtimeId"] : "";
76
+ if (!deviceToken || !runtimeId) {
77
+ throw new Error("Approval response missing token or runtimeId");
78
+ }
79
+ // Resolve the relay/gateway URL in exactly ONE place (D-02, D-03).
80
+ // Default to the caller-supplied gateway; if the server sent a valid
81
+ // `gateway_url`, use that instead. The server value is UNTRUSTED input —
82
+ // re-validate it through the same SSRF guards applied to `--gateway`.
83
+ let gatewayUrl = params.gatewayUrl;
84
+ const serverGatewayUrl = typeof tokenResponse["gateway_url"] === "string"
85
+ ? tokenResponse["gateway_url"]
86
+ : undefined;
87
+ if (serverGatewayUrl) {
88
+ try {
89
+ const parsed = parseCloudGatewayUrl(serverGatewayUrl);
90
+ validateGatewayNetworkTarget(parsed);
91
+ gatewayUrl = parsed.toString();
92
+ }
93
+ catch (err) {
94
+ // Fail loud-but-non-fatal: a bad server-supplied relay URL must not
95
+ // crash login. Warn and fall back to the configured `--gateway`.
96
+ const reason = err instanceof Error ? err.message : String(err);
97
+ process.stderr.write(`${params.binaryName}: ignoring invalid server-supplied relay URL (${reason}); using configured --gateway instead.\n`);
98
+ }
99
+ }
100
+ return { deviceToken, runtimeId, gatewayUrl };
101
+ }
102
+ if (status === "denied") {
103
+ clearInterval(spinnerInterval);
104
+ cursorTo(process.stdout, 0);
105
+ clearLine(process.stdout, 0);
106
+ process.stdout.write(`${params.binaryName}: Device request denied by user.\n`);
107
+ process.exit(1);
108
+ }
109
+ if (status === "expired") {
110
+ break;
111
+ }
112
+ if (status === "slow_down") {
113
+ pollIntervalMs += SLOW_DOWN_INCREMENT_MS;
114
+ continue;
115
+ }
116
+ // status === "pending" or unknown — keep polling
117
+ }
118
+ }
119
+ finally {
120
+ clearInterval(spinnerInterval);
121
+ }
122
+ // Timeout path (D-11)
123
+ cursorTo(process.stdout, 0);
124
+ clearLine(process.stdout, 0);
125
+ process.stdout.write(`${params.binaryName}: Authorization request timed out. Please run login again.\n`);
126
+ process.exit(1);
127
+ }
128
+ //# sourceMappingURL=device-flow.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"device-flow.js","sourceRoot":"","sources":["../src/device-flow.ts"],"names":[],"mappings":"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","sourcesContent":["// Device flow handler: code request, terminal display, polling loop, config save.\n// Implements RFC 8628 device authorization grant for the daemon's `gsd cloud login` command.\nimport { hostname as osHostname } from \"node:os\";\nimport { cursorTo, clearLine } from \"node:readline\";\nimport { parseCloudGatewayUrl, postJsonToValidatedGateway, validateGatewayNetworkTarget } from \"./cloud-config.js\";\n\nconst SPINNER_FRAMES = [\"⠋\", \"⠙\", \"⠹\", \"⠸\", \"⠼\", \"⠴\", \"⠦\", \"⠧\", \"⠇\", \"⠏\"];\nconst DEFAULT_POLL_INTERVAL_MS = 5_000;\nconst SLOW_DOWN_INCREMENT_MS = 5_000;\n\nexport interface DeviceFlowResult {\n deviceToken: string;\n runtimeId: string;\n /**\n * Resolved relay/gateway URL: the server-returned `gateway_url` from the approved\n * token-poll response when present AND valid, else the `gatewayUrl` the caller passed\n * (D-02 backward-compat — existing single-host `--gateway` users are unaffected).\n */\n gatewayUrl: string;\n}\n\nexport interface DeviceFlowParams {\n gatewayUrl: string;\n configPath: string;\n runtimeName?: string;\n binaryName: string;\n}\n\n/** Simple sleep helper. */\nconst sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));\n\n/**\n * Run the full RFC 8628 device authorization flow.\n * Requests a device code, displays the verification URL and user code in the terminal,\n * polls for approval with a spinner, and returns the device token on success.\n * Calls process.exit(1) on denial (D-12) or timeout (D-11).\n */\nexport async function runDeviceFlow(params: DeviceFlowParams): Promise<DeviceFlowResult> {\n const base = parseCloudGatewayUrl(params.gatewayUrl);\n\n // ---- Step 1: Request device code (D-01, DEVF-01) ----\n const codeUrl = new URL(base.pathname.replace(/\\/+$/, \"\") + \"/api/device/code\", base);\n const codeResponse = await postJsonToValidatedGateway(codeUrl, {\n hostname: osHostname(),\n os: process.platform,\n ...(params.runtimeName !== undefined ? { runtimeName: params.runtimeName } : {}),\n });\n\n const userCode = typeof codeResponse[\"userCode\"] === \"string\" ? codeResponse[\"userCode\"] : \"\";\n const deviceCode = typeof codeResponse[\"deviceCode\"] === \"string\" ? codeResponse[\"deviceCode\"] : \"\";\n const verificationUriComplete = typeof codeResponse[\"verificationUriComplete\"] === \"string\"\n ? codeResponse[\"verificationUriComplete\"]\n : \"\";\n const expiresIn = typeof codeResponse[\"expiresIn\"] === \"number\" ? codeResponse[\"expiresIn\"] : 600;\n\n if (!userCode || !deviceCode) {\n throw new Error(\"Device code response missing userCode or deviceCode\");\n }\n\n // ---- Step 2: Display to user (D-02) ----\n process.stdout.write(`\\n${params.binaryName}: Cloud Login\\n`);\n process.stdout.write(`\\nTo authorize this machine, open the following URL in your browser:\\n`);\n process.stdout.write(`\\n ${verificationUriComplete}\\n`);\n process.stdout.write(`\\nOr visit the gateway and enter the code manually:\\n`);\n process.stdout.write(`\\n ${base.origin}\\n`);\n process.stdout.write(`\\nYour code: ${userCode}\\n\\n`);\n\n // ---- Step 3: Polling loop (D-10, D-11, D-12) ----\n const tokenUrl = new URL(base.pathname.replace(/\\/+$/, \"\") + \"/api/device/token\", base);\n const expiresAt = Date.now() + expiresIn * 1_000;\n let pollIntervalMs = DEFAULT_POLL_INTERVAL_MS;\n let spinnerIndex = 0;\n\n const spinnerInterval = setInterval(() => {\n const frame = SPINNER_FRAMES[spinnerIndex % SPINNER_FRAMES.length] ?? \"⠋\";\n spinnerIndex++;\n const remaining = Math.max(0, Math.ceil((expiresAt - Date.now()) / 1_000));\n cursorTo(process.stdout, 0);\n clearLine(process.stdout, 0);\n process.stdout.write(` ${frame} Waiting for approval... (expires in ${remaining}s)`);\n }, 100);\n\n try {\n while (Date.now() < expiresAt) {\n await sleep(pollIntervalMs);\n\n if (Date.now() >= expiresAt) break;\n\n let tokenResponse: Record<string, unknown>;\n try {\n tokenResponse = await postJsonToValidatedGateway(tokenUrl, { deviceCode });\n } catch {\n // Network errors during polling are transient — keep trying until expiry.\n continue;\n }\n\n const status = typeof tokenResponse[\"status\"] === \"string\" ? tokenResponse[\"status\"] : \"\";\n\n if (status === \"approved\") {\n clearInterval(spinnerInterval);\n cursorTo(process.stdout, 0);\n clearLine(process.stdout, 0);\n process.stdout.write(`${params.binaryName}: Authorization approved!\\n`);\n\n const deviceToken = typeof tokenResponse[\"token\"] === \"string\" ? tokenResponse[\"token\"] : \"\";\n const runtimeId = typeof tokenResponse[\"runtimeId\"] === \"string\" ? tokenResponse[\"runtimeId\"] : \"\";\n if (!deviceToken || !runtimeId) {\n throw new Error(\"Approval response missing token or runtimeId\");\n }\n\n // Resolve the relay/gateway URL in exactly ONE place (D-02, D-03).\n // Default to the caller-supplied gateway; if the server sent a valid\n // `gateway_url`, use that instead. The server value is UNTRUSTED input —\n // re-validate it through the same SSRF guards applied to `--gateway`.\n let gatewayUrl = params.gatewayUrl;\n const serverGatewayUrl = typeof tokenResponse[\"gateway_url\"] === \"string\"\n ? tokenResponse[\"gateway_url\"]\n : undefined;\n if (serverGatewayUrl) {\n try {\n const parsed = parseCloudGatewayUrl(serverGatewayUrl);\n validateGatewayNetworkTarget(parsed);\n gatewayUrl = parsed.toString();\n } catch (err) {\n // Fail loud-but-non-fatal: a bad server-supplied relay URL must not\n // crash login. Warn and fall back to the configured `--gateway`.\n const reason = err instanceof Error ? err.message : String(err);\n process.stderr.write(\n `${params.binaryName}: ignoring invalid server-supplied relay URL (${reason}); using configured --gateway instead.\\n`,\n );\n }\n }\n\n return { deviceToken, runtimeId, gatewayUrl };\n }\n\n if (status === \"denied\") {\n clearInterval(spinnerInterval);\n cursorTo(process.stdout, 0);\n clearLine(process.stdout, 0);\n process.stdout.write(`${params.binaryName}: Device request denied by user.\\n`);\n process.exit(1);\n }\n\n if (status === \"expired\") {\n break;\n }\n\n if (status === \"slow_down\") {\n pollIntervalMs += SLOW_DOWN_INCREMENT_MS;\n continue;\n }\n\n // status === \"pending\" or unknown — keep polling\n }\n } finally {\n clearInterval(spinnerInterval);\n }\n\n // Timeout path (D-11)\n cursorTo(process.stdout, 0);\n clearLine(process.stdout, 0);\n process.stdout.write(`${params.binaryName}: Authorization request timed out. Please run login again.\\n`);\n process.exit(1);\n}\n"]}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=device-flow.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"device-flow.test.d.ts","sourceRoot":"","sources":["../src/device-flow.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,172 @@
1
+ // Device-flow gateway_url resolution tests: server-valid-wins, absent-falls-back,
2
+ // invalid-falls-back-no-throw. Drives runDeviceFlow against a local loopback HTTP server.
3
+ import assert from "node:assert/strict";
4
+ import { createServer } from "node:http";
5
+ import { mkdtempSync } from "node:fs";
6
+ import { tmpdir } from "node:os";
7
+ import { join } from "node:path";
8
+ import { test } from "node:test";
9
+ import { runDeviceFlow } from "./device-flow.js";
10
+ /**
11
+ * Stand up a loopback HTTP server implementing both device-flow endpoints.
12
+ * `/api/device/code` returns a code payload; `/api/device/token` returns an
13
+ * approved response whose `gateway_url` field is shaped per `gatewayCase`.
14
+ */
15
+ function startDeviceServer(gatewayCase) {
16
+ const server = createServer((req, res) => {
17
+ let body = "";
18
+ req.setEncoding("utf8");
19
+ req.on("data", (chunk) => {
20
+ body += chunk;
21
+ });
22
+ req.on("end", () => {
23
+ if (req.url === "/api/device/code") {
24
+ res.writeHead(200, { "content-type": "application/json" });
25
+ res.end(JSON.stringify({
26
+ userCode: "ABCD-1234",
27
+ deviceCode: "device-code-fixture",
28
+ verificationUriComplete: "https://example.test/verify?code=ABCD-1234",
29
+ expiresIn: 600,
30
+ }));
31
+ return;
32
+ }
33
+ if (req.url === "/api/device/token") {
34
+ const approved = {
35
+ status: "approved",
36
+ token: "device-token-fixture",
37
+ runtimeId: "rt-fixture",
38
+ };
39
+ if (gatewayCase.kind === "value") {
40
+ approved["gateway_url"] = gatewayCase.value;
41
+ }
42
+ res.writeHead(200, { "content-type": "application/json" });
43
+ res.end(JSON.stringify(approved));
44
+ return;
45
+ }
46
+ res.writeHead(404, { "content-type": "application/json" });
47
+ res.end(JSON.stringify({ error: "not found" }));
48
+ });
49
+ });
50
+ return new Promise((resolve, reject) => {
51
+ server.once("error", reject);
52
+ server.listen(0, "127.0.0.1", () => {
53
+ const address = server.address();
54
+ if (!address || typeof address !== "object") {
55
+ reject(new Error("failed to bind loopback test server"));
56
+ return;
57
+ }
58
+ resolve({ server, baseUrl: `http://127.0.0.1:${address.port}` });
59
+ });
60
+ });
61
+ }
62
+ function closeServer(server) {
63
+ return new Promise((resolve, reject) => server.close((err) => (err ? reject(err) : resolve())));
64
+ }
65
+ /** Capture stderr writes so the invalid-URL warning can be asserted. */
66
+ function captureStderr() {
67
+ const original = process.stderr.write.bind(process.stderr);
68
+ let captured = "";
69
+ process.stderr.write = ((chunk) => {
70
+ captured += typeof chunk === "string" ? chunk : String(chunk);
71
+ return true;
72
+ });
73
+ return {
74
+ restore: () => {
75
+ process.stderr.write = original;
76
+ },
77
+ output: () => captured,
78
+ };
79
+ }
80
+ function configPathFixture() {
81
+ return join(mkdtempSync(join(tmpdir(), "gsd-device-flow-")), "daemon.yaml");
82
+ }
83
+ test("device flow uses a valid server gateway_url over the passed --gateway", async (t) => {
84
+ let started;
85
+ try {
86
+ // A different, valid loopback URL than the login base — server value should win.
87
+ const serverGateway = "https://relay.example/gateway";
88
+ started = await startDeviceServer({ kind: "value", value: serverGateway });
89
+ }
90
+ catch (err) {
91
+ if (err.code === "EPERM") {
92
+ t.skip("loopback listen is blocked in this sandbox");
93
+ return;
94
+ }
95
+ throw err;
96
+ }
97
+ try {
98
+ const result = await runDeviceFlow({
99
+ gatewayUrl: started.baseUrl,
100
+ configPath: configPathFixture(),
101
+ binaryName: "gsd",
102
+ });
103
+ // Server-returned value wins, normalized (trailing slash stripped from path).
104
+ assert.equal(result.gatewayUrl, "https://relay.example/gateway");
105
+ assert.notEqual(result.gatewayUrl, started.baseUrl);
106
+ assert.equal(result.deviceToken, "device-token-fixture");
107
+ assert.equal(result.runtimeId, "rt-fixture");
108
+ }
109
+ finally {
110
+ await closeServer(started.server);
111
+ }
112
+ });
113
+ test("device flow falls back to --gateway when server omits gateway_url (backward-compat)", async (t) => {
114
+ let started;
115
+ try {
116
+ started = await startDeviceServer({ kind: "absent" });
117
+ }
118
+ catch (err) {
119
+ if (err.code === "EPERM") {
120
+ t.skip("loopback listen is blocked in this sandbox");
121
+ return;
122
+ }
123
+ throw err;
124
+ }
125
+ try {
126
+ const result = await runDeviceFlow({
127
+ gatewayUrl: started.baseUrl,
128
+ configPath: configPathFixture(),
129
+ binaryName: "gsd",
130
+ });
131
+ // Load-bearing invariant: with no server gateway_url, the passed --gateway is preserved.
132
+ assert.equal(result.gatewayUrl, started.baseUrl);
133
+ assert.equal(result.deviceToken, "device-token-fixture");
134
+ assert.equal(result.runtimeId, "rt-fixture");
135
+ }
136
+ finally {
137
+ await closeServer(started.server);
138
+ }
139
+ });
140
+ test("device flow falls back to --gateway when server gateway_url is invalid (no throw)", async (t) => {
141
+ let started;
142
+ try {
143
+ // A private-IP https URL fails validateGatewayNetworkTarget — must fall back, not crash.
144
+ started = await startDeviceServer({ kind: "value", value: "https://10.0.0.5/gateway" });
145
+ }
146
+ catch (err) {
147
+ if (err.code === "EPERM") {
148
+ t.skip("loopback listen is blocked in this sandbox");
149
+ return;
150
+ }
151
+ throw err;
152
+ }
153
+ const stderr = captureStderr();
154
+ let capturedWarning = "";
155
+ try {
156
+ const result = await runDeviceFlow({
157
+ gatewayUrl: started.baseUrl,
158
+ configPath: configPathFixture(),
159
+ binaryName: "gsd",
160
+ });
161
+ capturedWarning = stderr.output();
162
+ stderr.restore();
163
+ // Invalid server value is ignored; passed --gateway is used; login does not throw.
164
+ assert.equal(result.gatewayUrl, started.baseUrl);
165
+ assert.match(capturedWarning, /ignoring invalid server-supplied relay URL/);
166
+ }
167
+ finally {
168
+ stderr.restore();
169
+ await closeServer(started.server);
170
+ }
171
+ });
172
+ //# sourceMappingURL=device-flow.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"device-flow.test.js","sourceRoot":"","sources":["../src/device-flow.test.ts"],"names":[],"mappings":"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","sourcesContent":["// Device-flow gateway_url resolution tests: server-valid-wins, absent-falls-back,\n// invalid-falls-back-no-throw. Drives runDeviceFlow against a local loopback HTTP server.\nimport assert from \"node:assert/strict\";\nimport { createServer, type Server } from \"node:http\";\nimport { mkdtempSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { test } from \"node:test\";\nimport { runDeviceFlow } from \"./device-flow.js\";\n\n/** Server-side gateway_url behaviour for a single test case. */\ntype GatewayCase =\n | { kind: \"value\"; value: string }\n | { kind: \"absent\" };\n\n/**\n * Stand up a loopback HTTP server implementing both device-flow endpoints.\n * `/api/device/code` returns a code payload; `/api/device/token` returns an\n * approved response whose `gateway_url` field is shaped per `gatewayCase`.\n */\nfunction startDeviceServer(gatewayCase: GatewayCase): Promise<{ server: Server; baseUrl: string }> {\n const server = createServer((req, res) => {\n let body = \"\";\n req.setEncoding(\"utf8\");\n req.on(\"data\", (chunk) => {\n body += chunk;\n });\n req.on(\"end\", () => {\n if (req.url === \"/api/device/code\") {\n res.writeHead(200, { \"content-type\": \"application/json\" });\n res.end(JSON.stringify({\n userCode: \"ABCD-1234\",\n deviceCode: \"device-code-fixture\",\n verificationUriComplete: \"https://example.test/verify?code=ABCD-1234\",\n expiresIn: 600,\n }));\n return;\n }\n if (req.url === \"/api/device/token\") {\n const approved: Record<string, unknown> = {\n status: \"approved\",\n token: \"device-token-fixture\",\n runtimeId: \"rt-fixture\",\n };\n if (gatewayCase.kind === \"value\") {\n approved[\"gateway_url\"] = gatewayCase.value;\n }\n res.writeHead(200, { \"content-type\": \"application/json\" });\n res.end(JSON.stringify(approved));\n return;\n }\n res.writeHead(404, { \"content-type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"not found\" }));\n });\n });\n\n return new Promise((resolve, reject) => {\n server.once(\"error\", reject);\n server.listen(0, \"127.0.0.1\", () => {\n const address = server.address();\n if (!address || typeof address !== \"object\") {\n reject(new Error(\"failed to bind loopback test server\"));\n return;\n }\n resolve({ server, baseUrl: `http://127.0.0.1:${address.port}` });\n });\n });\n}\n\nfunction closeServer(server: Server): Promise<void> {\n return new Promise((resolve, reject) => server.close((err) => (err ? reject(err) : resolve())));\n}\n\n/** Capture stderr writes so the invalid-URL warning can be asserted. */\nfunction captureStderr(): { restore: () => void; output: () => string } {\n const original = process.stderr.write.bind(process.stderr);\n let captured = \"\";\n (process.stderr as unknown as { write: typeof process.stderr.write }).write = ((chunk: unknown) => {\n captured += typeof chunk === \"string\" ? chunk : String(chunk);\n return true;\n }) as typeof process.stderr.write;\n return {\n restore: () => {\n (process.stderr as unknown as { write: typeof process.stderr.write }).write = original;\n },\n output: () => captured,\n };\n}\n\nfunction configPathFixture(): string {\n return join(mkdtempSync(join(tmpdir(), \"gsd-device-flow-\")), \"daemon.yaml\");\n}\n\ntest(\"device flow uses a valid server gateway_url over the passed --gateway\", async (t) => {\n let started: { server: Server; baseUrl: string };\n try {\n // A different, valid loopback URL than the login base — server value should win.\n const serverGateway = \"https://relay.example/gateway\";\n started = await startDeviceServer({ kind: \"value\", value: serverGateway });\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 result = await runDeviceFlow({\n gatewayUrl: started.baseUrl,\n configPath: configPathFixture(),\n binaryName: \"gsd\",\n });\n // Server-returned value wins, normalized (trailing slash stripped from path).\n assert.equal(result.gatewayUrl, \"https://relay.example/gateway\");\n assert.notEqual(result.gatewayUrl, started.baseUrl);\n assert.equal(result.deviceToken, \"device-token-fixture\");\n assert.equal(result.runtimeId, \"rt-fixture\");\n } finally {\n await closeServer(started.server);\n }\n});\n\ntest(\"device flow falls back to --gateway when server omits gateway_url (backward-compat)\", async (t) => {\n let started: { server: Server; baseUrl: string };\n try {\n started = await startDeviceServer({ kind: \"absent\" });\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 result = await runDeviceFlow({\n gatewayUrl: started.baseUrl,\n configPath: configPathFixture(),\n binaryName: \"gsd\",\n });\n // Load-bearing invariant: with no server gateway_url, the passed --gateway is preserved.\n assert.equal(result.gatewayUrl, started.baseUrl);\n assert.equal(result.deviceToken, \"device-token-fixture\");\n assert.equal(result.runtimeId, \"rt-fixture\");\n } finally {\n await closeServer(started.server);\n }\n});\n\ntest(\"device flow falls back to --gateway when server gateway_url is invalid (no throw)\", async (t) => {\n let started: { server: Server; baseUrl: string };\n try {\n // A private-IP https URL fails validateGatewayNetworkTarget — must fall back, not crash.\n started = await startDeviceServer({ kind: \"value\", value: \"https://10.0.0.5/gateway\" });\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 const stderr = captureStderr();\n let capturedWarning = \"\";\n try {\n const result = await runDeviceFlow({\n gatewayUrl: started.baseUrl,\n configPath: configPathFixture(),\n binaryName: \"gsd\",\n });\n capturedWarning = stderr.output();\n stderr.restore();\n // Invalid server value is ignored; passed --gateway is used; login does not throw.\n assert.equal(result.gatewayUrl, started.baseUrl);\n assert.match(capturedWarning, /ignoring invalid server-supplied relay URL/);\n } finally {\n stderr.restore();\n await closeServer(started.server);\n }\n});\n"]}
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengsd/daemon",
3
- "version": "1.6.0",
3
+ "version": "1.7.0-dev.30c4ec2f",
4
4
  "description": "GSD daemon — background process for project monitoring and Discord integration",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -27,13 +27,13 @@
27
27
  },
28
28
  "scripts": {
29
29
  "build": "node ../../scripts/clean-package-dist.cjs && tsc",
30
- "test": "node --test dist/daemon.test.js dist/cloud-config.test.js dist/cloud-runtime.test.js dist/local-tool-executor.test.js dist/mcp-cli.test.js dist/message-batcher.test.js"
30
+ "test": "node --test dist/daemon.test.js dist/cloud-config.test.js dist/cloud-runtime.test.js dist/device-flow.test.js dist/local-tool-executor.test.js dist/mcp-cli.test.js dist/message-batcher.test.js"
31
31
  },
32
32
  "dependencies": {
33
33
  "@anthropic-ai/sdk": "^0.52.0",
34
- "@opengsd/contracts": "^1.6.0",
35
- "@opengsd/mcp-server": "^1.6.0",
36
- "@opengsd/rpc-client": "^1.6.0",
34
+ "@opengsd/contracts": "^1.7.0-dev.30c4ec2f",
35
+ "@opengsd/mcp-server": "^1.7.0-dev.30c4ec2f",
36
+ "@opengsd/rpc-client": "^1.7.0-dev.30c4ec2f",
37
37
  "discord.js": "^14.25.1",
38
38
  "ws": "^8.20.0",
39
39
  "yaml": "^2.8.0",