@neuralnomads/codenomad-dev 0.20.0-dev-20260909-855d2ee7 → 0.20.0-dev-20260911-ea37f97b

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 (177) hide show
  1. package/dist/api-types.js.map +1 -1
  2. package/dist/index.js +42 -31
  3. package/dist/index.js.map +1 -1
  4. package/dist/index.test.js +9 -1
  5. package/dist/index.test.js.map +1 -1
  6. package/dist/opencode/pruning-installation.js +65 -0
  7. package/dist/opencode/pruning-installation.js.map +1 -0
  8. package/dist/opencode/pruning-installation.test.js +101 -0
  9. package/dist/opencode/pruning-installation.test.js.map +1 -0
  10. package/dist/opencode/pruning-lifecycle.js +66 -0
  11. package/dist/opencode/pruning-lifecycle.js.map +1 -0
  12. package/dist/opencode/session-pruning/claim-fence.js +33 -0
  13. package/dist/opencode/session-pruning/claim-fence.js.map +1 -0
  14. package/dist/opencode/session-pruning/claim-fence.test.js +104 -0
  15. package/dist/opencode/session-pruning/claim-fence.test.js.map +1 -0
  16. package/dist/opencode/session-pruning/contract.js +40 -0
  17. package/dist/opencode/session-pruning/contract.js.map +1 -0
  18. package/dist/opencode/session-pruning/database-path.js +17 -0
  19. package/dist/opencode/session-pruning/database-path.js.map +1 -0
  20. package/dist/opencode/session-pruning/database-path.test.js +22 -0
  21. package/dist/opencode/session-pruning/database-path.test.js.map +1 -0
  22. package/dist/opencode/session-pruning/desktop-plugin.js +14 -0
  23. package/dist/opencode/session-pruning/desktop-plugin.js.map +1 -0
  24. package/dist/opencode/session-pruning/index.js +2 -0
  25. package/dist/opencode/session-pruning/index.js.map +1 -0
  26. package/dist/opencode/session-pruning/isolated-store.js +18 -0
  27. package/dist/opencode/session-pruning/isolated-store.js.map +1 -0
  28. package/dist/opencode/session-pruning/planner.js +36 -0
  29. package/dist/opencode/session-pruning/planner.js.map +1 -0
  30. package/dist/opencode/session-pruning/plugin.js +45 -0
  31. package/dist/opencode/session-pruning/plugin.js.map +1 -0
  32. package/dist/opencode/session-pruning/presence.js +42 -0
  33. package/dist/opencode/session-pruning/presence.js.map +1 -0
  34. package/dist/opencode/session-pruning/preview-store.js +21 -0
  35. package/dist/opencode/session-pruning/preview-store.js.map +1 -0
  36. package/dist/opencode/session-pruning/pruning.test.js +172 -0
  37. package/dist/opencode/session-pruning/pruning.test.js.map +1 -0
  38. package/dist/opencode/session-pruning/revision.js +18 -0
  39. package/dist/opencode/session-pruning/revision.js.map +1 -0
  40. package/dist/opencode/session-pruning/service.js +56 -0
  41. package/dist/opencode/session-pruning/service.js.map +1 -0
  42. package/dist/opencode/session-pruning/service.test.js +74 -0
  43. package/dist/opencode/session-pruning/service.test.js.map +1 -0
  44. package/dist/opencode/session-pruning/storage-path.js +6 -0
  45. package/dist/opencode/session-pruning/storage-path.js.map +1 -0
  46. package/dist/opencode/session-pruning/transaction.js +53 -0
  47. package/dist/opencode/session-pruning/transaction.js.map +1 -0
  48. package/dist/opencode/session-pruning/tui-reload.js +84 -0
  49. package/dist/opencode/session-pruning/tui-reload.js.map +1 -0
  50. package/dist/opencode/session-pruning/tui-reload.native.js +269 -0
  51. package/dist/opencode/session-pruning/tui-reload.native.js.map +1 -0
  52. package/dist/opencode/session-pruning/tui-reload.test.js +71 -0
  53. package/dist/opencode/session-pruning/tui-reload.test.js.map +1 -0
  54. package/dist/opencode/session-pruning/tui.js +12 -0
  55. package/dist/opencode/session-pruning/tui.js.map +1 -0
  56. package/dist/plugins/session-pruning/plugin.mjs +26559 -0
  57. package/dist/server/__tests__/instance-proxy.test.js +10 -5
  58. package/dist/server/__tests__/instance-proxy.test.js.map +1 -1
  59. package/dist/server/__tests__/listener-base-url.test.js +26 -1
  60. package/dist/server/__tests__/listener-base-url.test.js.map +1 -1
  61. package/dist/server/__tests__/network-addresses.test.js +40 -0
  62. package/dist/server/__tests__/network-addresses.test.js.map +1 -1
  63. package/dist/server/__tests__/network-host.test.js +34 -0
  64. package/dist/server/__tests__/network-host.test.js.map +1 -0
  65. package/dist/server/__tests__/tls-ipv6.test.js +54 -0
  66. package/dist/server/__tests__/tls-ipv6.test.js.map +1 -0
  67. package/dist/server/http-server.js +8 -6
  68. package/dist/server/http-server.js.map +1 -1
  69. package/dist/server/listener-base-url.js +12 -3
  70. package/dist/server/listener-base-url.js.map +1 -1
  71. package/dist/server/network-addresses.js +20 -42
  72. package/dist/server/network-addresses.js.map +1 -1
  73. package/dist/server/network-host.js +62 -0
  74. package/dist/server/network-host.js.map +1 -0
  75. package/dist/server/routes/meta.js +2 -4
  76. package/dist/server/routes/meta.js.map +1 -1
  77. package/dist/server/routes/session-pruning.js +45 -0
  78. package/dist/server/routes/session-pruning.js.map +1 -0
  79. package/dist/server/routes/session-pruning.test.js +98 -0
  80. package/dist/server/routes/session-pruning.test.js.map +1 -0
  81. package/dist/server/tls.js +52 -32
  82. package/dist/server/tls.js.map +1 -1
  83. package/dist/workspaces/manager.js +1 -0
  84. package/dist/workspaces/manager.js.map +1 -1
  85. package/package.json +6 -3
  86. package/public/assets/App-0-FcHzPS.js +44 -0
  87. package/public/assets/App-0-FcHzPS.js.map +1 -0
  88. package/public/assets/FilesTab-CRpGDs1B.js +3 -0
  89. package/public/assets/{FilesTab-z7UrjcAQ.js.map → FilesTab-CRpGDs1B.js.map} +1 -1
  90. package/public/assets/{GitChangesTab-C04bYKhs.js → GitChangesTab-DxFebTiq.js} +3 -3
  91. package/public/assets/{GitChangesTab-C04bYKhs.js.map → GitChangesTab-DxFebTiq.js.map} +1 -1
  92. package/public/assets/{SplitFilePanel-B29MqCuU.js → SplitFilePanel-DgTMrrO-.js} +2 -2
  93. package/public/assets/{SplitFilePanel-B29MqCuU.js.map → SplitFilePanel-DgTMrrO-.js.map} +1 -1
  94. package/public/assets/{StatusTab-wUKfwnFf.js → StatusTab-LKUA9cbH.js} +2 -2
  95. package/public/assets/{StatusTab-wUKfwnFf.js.map → StatusTab-LKUA9cbH.js.map} +1 -1
  96. package/public/assets/{align-justify-glU678D9.js → align-justify-v_yPXSys.js} +2 -2
  97. package/public/assets/{align-justify-glU678D9.js.map → align-justify-v_yPXSys.js.map} +1 -1
  98. package/public/assets/{bundle-full-BrsCElYv.js → bundle-full-HzL_Jtqk.js} +2 -2
  99. package/public/assets/{bundle-full-BrsCElYv.js.map → bundle-full-HzL_Jtqk.js.map} +1 -1
  100. package/public/assets/{client-state-j9Iu7F-E.js → client-state-CF-PJ0GG.js} +2 -2
  101. package/public/assets/{client-state-j9Iu7F-E.js.map → client-state-CF-PJ0GG.js.map} +1 -1
  102. package/public/assets/{diff-viewer-B0VS1voD.js → diff-viewer-BFDQBDVy.js} +2 -2
  103. package/public/assets/{diff-viewer-B0VS1voD.js.map → diff-viewer-BFDQBDVy.js.map} +1 -1
  104. package/public/assets/index-8iPlm2r0.js +2 -0
  105. package/public/assets/index-8iPlm2r0.js.map +1 -0
  106. package/public/assets/index-B185mC7m.js +2 -0
  107. package/public/assets/index-B185mC7m.js.map +1 -0
  108. package/public/assets/index-Bbj1xF8i.js +2 -0
  109. package/public/assets/index-Bbj1xF8i.js.map +1 -0
  110. package/public/assets/index-BsmFN3k1.js +2 -0
  111. package/public/assets/index-BsmFN3k1.js.map +1 -0
  112. package/public/assets/index-Bz_OQJYA.css +1 -0
  113. package/public/assets/index-C52CfqK3.js +2 -0
  114. package/public/assets/index-C52CfqK3.js.map +1 -0
  115. package/public/assets/index-CHYmKl10.js +2 -0
  116. package/public/assets/index-CHYmKl10.js.map +1 -0
  117. package/public/assets/index-CTKYD_VJ.js +2 -0
  118. package/public/assets/index-CTKYD_VJ.js.map +1 -0
  119. package/public/assets/index-D2UHSOG5.js +2 -0
  120. package/public/assets/{index-ClaHfu-1.js.map → index-D2UHSOG5.js.map} +1 -1
  121. package/public/assets/index-DDoB_Z36.js +2 -0
  122. package/public/assets/index-DDoB_Z36.js.map +1 -0
  123. package/public/assets/{index-BzAEHHap.js → index-DcwAtZw-.js} +3 -3
  124. package/public/assets/index-DcwAtZw-.js.map +1 -0
  125. package/public/assets/index-DxUD3RYr.js +2 -0
  126. package/public/assets/index-DxUD3RYr.js.map +1 -0
  127. package/public/assets/{index-B6bX3AMT.js → index-RK9gf0uk.js} +2 -2
  128. package/public/assets/{index-B6bX3AMT.js.map → index-RK9gf0uk.js.map} +1 -1
  129. package/public/assets/{loading-CYiiwb08.js → loading-BSB82snY.js} +2 -2
  130. package/public/assets/{loading-CYiiwb08.js.map → loading-BSB82snY.js.map} +1 -1
  131. package/public/assets/main-CZztwrfv.js +48 -0
  132. package/public/assets/main-CZztwrfv.js.map +1 -0
  133. package/public/assets/{markdown-DWvSdCZz.js → markdown-DtidoL2Z.js} +4 -4
  134. package/public/assets/{markdown-DWvSdCZz.js.map → markdown-DtidoL2Z.js.map} +1 -1
  135. package/public/assets/{monaco-viewer-Cc9bWg7I.js → monaco-viewer-CpxDfB-6.js} +2 -2
  136. package/public/assets/monaco-viewer-CpxDfB-6.js.map +1 -0
  137. package/public/assets/{startup-state-settings-card-2KHpBL8U.js → startup-state-settings-card-BY741Rt0.js} +2 -2
  138. package/public/assets/{startup-state-settings-card-2KHpBL8U.js.map → startup-state-settings-card-BY741Rt0.js.map} +1 -1
  139. package/public/assets/{terminal-square-lzHuj_of.js → terminal-square-BdC_jwm3.js} +2 -2
  140. package/public/assets/{terminal-square-lzHuj_of.js.map → terminal-square-BdC_jwm3.js.map} +1 -1
  141. package/public/assets/{tool-call-DjkszeT4.js → tool-call-DbtCsJ4x.js} +10 -10
  142. package/public/assets/{tool-call-DjkszeT4.js.map → tool-call-DbtCsJ4x.js.map} +1 -1
  143. package/public/assets/{unified-picker-5JmIM6a0.js → unified-picker-C0eo1chJ.js} +2 -2
  144. package/public/assets/{unified-picker-5JmIM6a0.js.map → unified-picker-C0eo1chJ.js.map} +1 -1
  145. package/public/assets/{wrap-text-N0Cav2Vp.js → wrap-text-BuMtdvMx.js} +2 -2
  146. package/public/assets/{wrap-text-N0Cav2Vp.js.map → wrap-text-BuMtdvMx.js.map} +1 -1
  147. package/public/index.html +4 -4
  148. package/public/loading.html +4 -4
  149. package/public/sw.js +1 -1
  150. package/public/sw.js.map +1 -1
  151. package/public/assets/App-Nlt9zV-v.js +0 -43
  152. package/public/assets/App-Nlt9zV-v.js.map +0 -1
  153. package/public/assets/FilesTab-z7UrjcAQ.js +0 -3
  154. package/public/assets/index-BIRLnDWY.js +0 -2
  155. package/public/assets/index-BIRLnDWY.js.map +0 -1
  156. package/public/assets/index-BuScpzpP.js +0 -2
  157. package/public/assets/index-BuScpzpP.js.map +0 -1
  158. package/public/assets/index-BzAEHHap.js.map +0 -1
  159. package/public/assets/index-C-DZt-oz.css +0 -1
  160. package/public/assets/index-C9RXVQl9.js +0 -2
  161. package/public/assets/index-C9RXVQl9.js.map +0 -1
  162. package/public/assets/index-ClaHfu-1.js +0 -2
  163. package/public/assets/index-DBx5htn3.js +0 -2
  164. package/public/assets/index-DBx5htn3.js.map +0 -1
  165. package/public/assets/index-DHHMf3u1.js +0 -2
  166. package/public/assets/index-DHHMf3u1.js.map +0 -1
  167. package/public/assets/index-DN4Kstsd.js +0 -2
  168. package/public/assets/index-DN4Kstsd.js.map +0 -1
  169. package/public/assets/index-DbMyNDyo.js +0 -2
  170. package/public/assets/index-DbMyNDyo.js.map +0 -1
  171. package/public/assets/index-rcsmuqww.js +0 -2
  172. package/public/assets/index-rcsmuqww.js.map +0 -1
  173. package/public/assets/index-uz2QFRlk.js +0 -2
  174. package/public/assets/index-uz2QFRlk.js.map +0 -1
  175. package/public/assets/main-OmXs7qtG.js +0 -48
  176. package/public/assets/main-OmXs7qtG.js.map +0 -1
  177. package/public/assets/monaco-viewer-Cc9bWg7I.js.map +0 -1
@@ -1,7 +1,16 @@
1
+ import { isIP } from "node:net";
2
+ import { formatHostForUrl, isLoopbackHost, isWildcardHost, normalizeNetworkHost } from "./network-host";
3
+ export function resolvePreferredRemoteListener(input) {
4
+ return input.httpsStart ?? input.httpStart ?? null;
5
+ }
1
6
  export function resolvePluginBaseUrl(input) {
2
7
  const loopbackListener = [input.httpStart, input.httpsStart].find((listener) => listener && acceptsLoopback(listener.bindHost));
3
8
  if (loopbackListener) {
4
- return `${loopbackListener.protocol}://127.0.0.1:${loopbackListener.port}`;
9
+ const bindHost = normalizeNetworkHost(loopbackListener.bindHost);
10
+ const loopbackHost = isWildcardHost(bindHost)
11
+ ? isIP(bindHost) === 6 ? "::1" : "127.0.0.1"
12
+ : bindHost;
13
+ return `${loopbackListener.protocol}://${formatHostForUrl(loopbackHost)}:${loopbackListener.port}`;
5
14
  }
6
15
  if (input.remoteUrl) {
7
16
  return input.remoteUrl;
@@ -10,7 +19,7 @@ export function resolvePluginBaseUrl(input) {
10
19
  if (!fallbackListener) {
11
20
  throw new Error("No listeners started");
12
21
  }
13
- return `${fallbackListener.protocol}://${fallbackListener.bindHost}:${fallbackListener.port}`;
22
+ return `${fallbackListener.protocol}://${formatHostForUrl(fallbackListener.bindHost)}:${fallbackListener.port}`;
14
23
  }
15
24
  export function resolveAutomationBridgeUrl(listener) {
16
25
  if (listener.protocol !== "http" || !acceptsLoopback(listener.bindHost)) {
@@ -19,6 +28,6 @@ export function resolveAutomationBridgeUrl(listener) {
19
28
  return `http://127.0.0.1:${listener.port}`;
20
29
  }
21
30
  function acceptsLoopback(bindHost) {
22
- return bindHost === "0.0.0.0" || bindHost === "::" || bindHost === "localhost" || bindHost === "::1" || bindHost.startsWith("127.");
31
+ return bindHost === "localhost" || isWildcardHost(bindHost) || isLoopbackHost(bindHost);
23
32
  }
24
33
  //# sourceMappingURL=listener-base-url.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"listener-base-url.js","sourceRoot":"","sources":["../../src/server/listener-base-url.ts"],"names":[],"mappings":"AAYA,MAAM,UAAU,oBAAoB,CAAC,KAAgC;IACnE,MAAM,gBAAgB,GAAG,CAAC,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,IAAI,eAAe,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAA;IAC/H,IAAI,gBAAgB,EAAE,CAAC;QACrB,OAAO,GAAG,gBAAgB,CAAC,QAAQ,gBAAgB,gBAAgB,CAAC,IAAI,EAAE,CAAA;IAC5E,CAAC;IAED,IAAI,KAAK,CAAC,SAAS,EAAE,CAAC;QACpB,OAAO,KAAK,CAAC,SAAS,CAAA;IACxB,CAAC;IAED,MAAM,gBAAgB,GAAG,KAAK,CAAC,SAAS,IAAI,KAAK,CAAC,UAAU,CAAA;IAC5D,IAAI,CAAC,gBAAgB,EAAE,CAAC;QACtB,MAAM,IAAI,KAAK,CAAC,sBAAsB,CAAC,CAAA;IACzC,CAAC;IAED,OAAO,GAAG,gBAAgB,CAAC,QAAQ,MAAM,gBAAgB,CAAC,QAAQ,IAAI,gBAAgB,CAAC,IAAI,EAAE,CAAA;AAC/F,CAAC;AAED,MAAM,UAAU,0BAA0B,CAAC,QAAqC;IAC9E,IAAI,QAAQ,CAAC,QAAQ,KAAK,MAAM,IAAI,CAAC,eAAe,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;QACxE,MAAM,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAA;IACrE,CAAC;IACD,OAAO,oBAAoB,QAAQ,CAAC,IAAI,EAAE,CAAA;AAC5C,CAAC;AAED,SAAS,eAAe,CAAC,QAAgB;IACvC,OAAO,QAAQ,KAAK,SAAS,IAAI,QAAQ,KAAK,IAAI,IAAI,QAAQ,KAAK,WAAW,IAAI,QAAQ,KAAK,KAAK,IAAI,QAAQ,CAAC,UAAU,CAAC,MAAM,CAAC,CAAA;AACrI,CAAC","sourcesContent":["export interface StartedListenerBaseUrlInput {\n protocol: \"http\" | \"https\"\n bindHost: string\n port: number\n}\n\nexport interface ResolvePluginBaseUrlInput {\n httpStart?: StartedListenerBaseUrlInput | null\n httpsStart?: StartedListenerBaseUrlInput | null\n remoteUrl?: string\n}\n\nexport function resolvePluginBaseUrl(input: ResolvePluginBaseUrlInput): string {\n const loopbackListener = [input.httpStart, input.httpsStart].find((listener) => listener && acceptsLoopback(listener.bindHost))\n if (loopbackListener) {\n return `${loopbackListener.protocol}://127.0.0.1:${loopbackListener.port}`\n }\n\n if (input.remoteUrl) {\n return input.remoteUrl\n }\n\n const fallbackListener = input.httpStart ?? input.httpsStart\n if (!fallbackListener) {\n throw new Error(\"No listeners started\")\n }\n\n return `${fallbackListener.protocol}://${fallbackListener.bindHost}:${fallbackListener.port}`\n}\n\nexport function resolveAutomationBridgeUrl(listener: StartedListenerBaseUrlInput): string {\n if (listener.protocol !== \"http\" || !acceptsLoopback(listener.bindHost)) {\n throw new Error(\"Developer Mode requires a loopback HTTP listener\")\n }\n return `http://127.0.0.1:${listener.port}`\n}\n\nfunction acceptsLoopback(bindHost: string): boolean {\n return bindHost === \"0.0.0.0\" || bindHost === \"::\" || bindHost === \"localhost\" || bindHost === \"::1\" || bindHost.startsWith(\"127.\")\n}\n"]}
1
+ {"version":3,"file":"listener-base-url.js","sourceRoot":"","sources":["../../src/server/listener-base-url.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAA;AAC/B,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAA;AAcvG,MAAM,UAAU,8BAA8B,CAC5C,KAAkE;IAElE,OAAO,KAAK,CAAC,UAAU,IAAI,KAAK,CAAC,SAAS,IAAI,IAAI,CAAA;AACpD,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,KAAgC;IACnE,MAAM,gBAAgB,GAAG,CAAC,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,IAAI,eAAe,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAA;IAC/H,IAAI,gBAAgB,EAAE,CAAC;QACrB,MAAM,QAAQ,GAAG,oBAAoB,CAAC,gBAAgB,CAAC,QAAQ,CAAC,CAAA;QAChE,MAAM,YAAY,GAAG,cAAc,CAAC,QAAQ,CAAC;YAC3C,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,WAAW;YAC5C,CAAC,CAAC,QAAQ,CAAA;QACZ,OAAO,GAAG,gBAAgB,CAAC,QAAQ,MAAM,gBAAgB,CAAC,YAAY,CAAC,IAAI,gBAAgB,CAAC,IAAI,EAAE,CAAA;IACpG,CAAC;IAED,IAAI,KAAK,CAAC,SAAS,EAAE,CAAC;QACpB,OAAO,KAAK,CAAC,SAAS,CAAA;IACxB,CAAC;IAED,MAAM,gBAAgB,GAAG,KAAK,CAAC,SAAS,IAAI,KAAK,CAAC,UAAU,CAAA;IAC5D,IAAI,CAAC,gBAAgB,EAAE,CAAC;QACtB,MAAM,IAAI,KAAK,CAAC,sBAAsB,CAAC,CAAA;IACzC,CAAC;IAED,OAAO,GAAG,gBAAgB,CAAC,QAAQ,MAAM,gBAAgB,CAAC,gBAAgB,CAAC,QAAQ,CAAC,IAAI,gBAAgB,CAAC,IAAI,EAAE,CAAA;AACjH,CAAC;AAED,MAAM,UAAU,0BAA0B,CAAC,QAAqC;IAC9E,IAAI,QAAQ,CAAC,QAAQ,KAAK,MAAM,IAAI,CAAC,eAAe,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;QACxE,MAAM,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAA;IACrE,CAAC;IACD,OAAO,oBAAoB,QAAQ,CAAC,IAAI,EAAE,CAAA;AAC5C,CAAC;AAED,SAAS,eAAe,CAAC,QAAgB;IACvC,OAAO,QAAQ,KAAK,WAAW,IAAI,cAAc,CAAC,QAAQ,CAAC,IAAI,cAAc,CAAC,QAAQ,CAAC,CAAA;AACzF,CAAC","sourcesContent":["import { isIP } from \"node:net\"\nimport { formatHostForUrl, isLoopbackHost, isWildcardHost, normalizeNetworkHost } from \"./network-host\"\n\nexport interface StartedListenerBaseUrlInput {\n protocol: \"http\" | \"https\"\n bindHost: string\n port: number\n}\n\nexport interface ResolvePluginBaseUrlInput {\n httpStart?: StartedListenerBaseUrlInput | null\n httpsStart?: StartedListenerBaseUrlInput | null\n remoteUrl?: string\n}\n\nexport function resolvePreferredRemoteListener(\n input: Pick<ResolvePluginBaseUrlInput, \"httpStart\" | \"httpsStart\">,\n): StartedListenerBaseUrlInput | null {\n return input.httpsStart ?? input.httpStart ?? null\n}\n\nexport function resolvePluginBaseUrl(input: ResolvePluginBaseUrlInput): string {\n const loopbackListener = [input.httpStart, input.httpsStart].find((listener) => listener && acceptsLoopback(listener.bindHost))\n if (loopbackListener) {\n const bindHost = normalizeNetworkHost(loopbackListener.bindHost)\n const loopbackHost = isWildcardHost(bindHost)\n ? isIP(bindHost) === 6 ? \"::1\" : \"127.0.0.1\"\n : bindHost\n return `${loopbackListener.protocol}://${formatHostForUrl(loopbackHost)}:${loopbackListener.port}`\n }\n\n if (input.remoteUrl) {\n return input.remoteUrl\n }\n\n const fallbackListener = input.httpStart ?? input.httpsStart\n if (!fallbackListener) {\n throw new Error(\"No listeners started\")\n }\n\n return `${fallbackListener.protocol}://${formatHostForUrl(fallbackListener.bindHost)}:${fallbackListener.port}`\n}\n\nexport function resolveAutomationBridgeUrl(listener: StartedListenerBaseUrlInput): string {\n if (listener.protocol !== \"http\" || !acceptsLoopback(listener.bindHost)) {\n throw new Error(\"Developer Mode requires a loopback HTTP listener\")\n }\n return `http://127.0.0.1:${listener.port}`\n}\n\nfunction acceptsLoopback(bindHost: string): boolean {\n return bindHost === \"localhost\" || isWildcardHost(bindHost) || isLoopbackHost(bindHost)\n}\n"]}
@@ -1,51 +1,40 @@
1
1
  import os from "os";
2
+ import { isIP } from "node:net";
3
+ import { formatHostForUrl, isLoopbackHost, isWildcardHost, normalizeNetworkHost } from "./network-host";
2
4
  export function resolveNetworkAddresses(args) {
3
- const { host, protocol, port } = args;
5
+ const { protocol, port } = args;
6
+ const host = normalizeNetworkHost(args.host);
4
7
  const interfaces = os.networkInterfaces();
5
8
  const seen = new Set();
6
9
  const results = [];
7
10
  const addAddress = (ip, scope) => {
8
- if (!ip || ip === "0.0.0.0")
11
+ const normalizedIp = normalizeNetworkHost(ip);
12
+ const ipVersion = isIP(normalizedIp);
13
+ if (!ipVersion || isWildcardHost(normalizedIp) || (ipVersion === 6 && isLinkLocalIPv6(normalizedIp)))
9
14
  return;
10
- const key = `ipv4-${ip}`;
15
+ const family = ipVersion === 6 ? "ipv6" : "ipv4";
16
+ const key = `${family}-${normalizedIp}`;
11
17
  if (seen.has(key))
12
18
  return;
13
19
  seen.add(key);
14
- results.push({ ip, family: "ipv4", scope, remoteUrl: `${protocol}://${ip}:${port}` });
20
+ results.push({ ip: normalizedIp, family, scope, remoteUrl: `${protocol}://${formatHostForUrl(normalizedIp)}:${port}` });
15
21
  };
16
- const normalizeFamily = (value) => {
17
- if (typeof value === "string") {
18
- const lowered = value.toLowerCase();
19
- if (lowered === "ipv4") {
20
- return "ipv4";
21
- }
22
- }
23
- if (value === 4)
24
- return "ipv4";
25
- return null;
26
- };
27
- if (host === "0.0.0.0") {
28
- // Enumerate system interfaces (IPv4 only)
22
+ if (isWildcardHost(host)) {
23
+ const wildcardVersion = isIP(host);
29
24
  for (const entries of Object.values(interfaces)) {
30
25
  if (!entries)
31
26
  continue;
32
27
  for (const entry of entries) {
33
- const family = normalizeFamily(entry.family);
34
- if (!family)
35
- continue;
36
- if (!entry.address || entry.address === "0.0.0.0")
28
+ const entryVersion = isIP(normalizeNetworkHost(entry.address));
29
+ if (entryVersion !== wildcardVersion && !(wildcardVersion === 6 && entryVersion === 4))
37
30
  continue;
38
31
  const scope = entry.internal ? "loopback" : "external";
39
32
  addAddress(entry.address, scope);
40
33
  }
41
34
  }
42
35
  }
43
- // Always include loopback address
44
- addAddress("127.0.0.1", "loopback");
45
- // Include explicitly configured host if it was IPv4
46
- if (isIPv4Address(host) && host !== "0.0.0.0") {
47
- const isLoopback = host.startsWith("127.");
48
- addAddress(host, isLoopback ? "loopback" : "external");
36
+ else if (isIP(host)) {
37
+ addAddress(host, isLoopbackHost(host) ? "loopback" : "external");
49
38
  }
50
39
  const scopeWeight = { external: 0, internal: 1, loopback: 2 };
51
40
  return results.sort((a, b) => {
@@ -93,23 +82,12 @@ function isPrivateIPv4(ip) {
93
82
  return first === 172 && second >= 16 && second <= 31;
94
83
  }
95
84
  function parseIPv4(value) {
96
- if (!isIPv4Address(value))
85
+ if (isIP(value) !== 4)
97
86
  return null;
98
87
  return value.split(".").map((part) => Number(part));
99
88
  }
100
- function isIPv4Address(value) {
101
- if (!value)
102
- return false;
103
- const parts = value.split(".");
104
- if (parts.length !== 4)
105
- return false;
106
- return parts.every((part) => {
107
- if (part.length === 0 || part.length > 3)
108
- return false;
109
- if (!/^[0-9]+$/.test(part))
110
- return false;
111
- const num = Number(part);
112
- return Number.isInteger(num) && num >= 0 && num <= 255;
113
- });
89
+ function isLinkLocalIPv6(ip) {
90
+ const firstSegment = Number.parseInt(ip.split(":", 1)[0], 16);
91
+ return Number.isInteger(firstSegment) && firstSegment >= 0xfe80 && firstSegment <= 0xfebf;
114
92
  }
115
93
  //# sourceMappingURL=network-addresses.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"network-addresses.js","sourceRoot":"","sources":["../../src/server/network-addresses.ts"],"names":[],"mappings":"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","sourcesContent":["import os from \"os\"\nimport type { NetworkAddress } from \"../api-types\"\n\nexport interface ResolvedRemoteAddresses {\n all: NetworkAddress[]\n userVisible: NetworkAddress[]\n primaryRemoteUrl?: string\n}\n\nexport function resolveNetworkAddresses(args: {\n host: string\n protocol: \"http\" | \"https\"\n port: number\n}): NetworkAddress[] {\n const { host, protocol, port } = args\n const interfaces = os.networkInterfaces()\n const seen = new Set<string>()\n const results: NetworkAddress[] = []\n\n const addAddress = (ip: string, scope: NetworkAddress[\"scope\"]) => {\n if (!ip || ip === \"0.0.0.0\") return\n const key = `ipv4-${ip}`\n if (seen.has(key)) return\n seen.add(key)\n results.push({ ip, family: \"ipv4\", scope, remoteUrl: `${protocol}://${ip}:${port}` })\n }\n\n const normalizeFamily = (value: string | number) => {\n if (typeof value === \"string\") {\n const lowered = value.toLowerCase()\n if (lowered === \"ipv4\") {\n return \"ipv4\" as const\n }\n }\n if (value === 4) return \"ipv4\" as const\n return null\n }\n\n if (host === \"0.0.0.0\") {\n // Enumerate system interfaces (IPv4 only)\n for (const entries of Object.values(interfaces)) {\n if (!entries) continue\n for (const entry of entries) {\n const family = normalizeFamily(entry.family)\n if (!family) continue\n if (!entry.address || entry.address === \"0.0.0.0\") continue\n const scope: NetworkAddress[\"scope\"] = entry.internal ? \"loopback\" : \"external\"\n addAddress(entry.address, scope)\n }\n }\n }\n\n // Always include loopback address\n addAddress(\"127.0.0.1\", \"loopback\")\n\n // Include explicitly configured host if it was IPv4\n if (isIPv4Address(host) && host !== \"0.0.0.0\") {\n const isLoopback = host.startsWith(\"127.\")\n addAddress(host, isLoopback ? \"loopback\" : \"external\")\n }\n\n const scopeWeight: Record<NetworkAddress[\"scope\"], number> = { external: 0, internal: 1, loopback: 2 }\n\n return results.sort((a, b) => {\n const scopeDelta = scopeWeight[a.scope] - scopeWeight[b.scope]\n if (scopeDelta !== 0) return scopeDelta\n\n return 0\n })\n}\n\nexport function resolveRemoteAddresses(args: {\n host: string\n protocol: \"http\" | \"https\"\n port: number\n}): ResolvedRemoteAddresses {\n const all = resolveNetworkAddresses(args)\n const userVisible = sortUserVisibleAddresses(all.filter((address) => address.scope === \"external\"))\n return {\n all,\n userVisible,\n primaryRemoteUrl: userVisible[0]?.remoteUrl,\n }\n}\n\nfunction sortUserVisibleAddresses(addresses: NetworkAddress[]): NetworkAddress[] {\n return [...addresses].sort((left, right) => getUserVisiblePriority(left.ip) - getUserVisiblePriority(right.ip))\n}\n\nfunction getUserVisiblePriority(ip: string): number {\n if (isPrivateIPv4(ip)) return 0\n if (isLinkLocalIPv4(ip)) return 2\n return 1\n}\n\nfunction isLinkLocalIPv4(ip: string): boolean {\n const octets = parseIPv4(ip)\n if (!octets) return false\n const [first, second] = octets\n return first === 169 && second === 254\n}\n\nfunction isPrivateIPv4(ip: string): boolean {\n const octets = parseIPv4(ip)\n if (!octets) return false\n const [first, second] = octets\n\n if (first === 10) return true\n if (first === 192 && second === 168) return true\n return first === 172 && second >= 16 && second <= 31\n}\n\nfunction parseIPv4(value: string): number[] | null {\n if (!isIPv4Address(value)) return null\n return value.split(\".\").map((part) => Number(part))\n}\n\nfunction isIPv4Address(value: string | undefined): value is string {\n if (!value) return false\n const parts = value.split(\".\")\n if (parts.length !== 4) return false\n return parts.every((part) => {\n if (part.length === 0 || part.length > 3) return false\n if (!/^[0-9]+$/.test(part)) return false\n const num = Number(part)\n return Number.isInteger(num) && num >= 0 && num <= 255\n })\n}\n"]}
1
+ {"version":3,"file":"network-addresses.js","sourceRoot":"","sources":["../../src/server/network-addresses.ts"],"names":[],"mappings":"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","sourcesContent":["import os from \"os\"\nimport { isIP } from \"node:net\"\nimport type { NetworkAddress } from \"../api-types\"\nimport { formatHostForUrl, isLoopbackHost, isWildcardHost, normalizeNetworkHost } from \"./network-host\"\n\nexport interface ResolvedRemoteAddresses {\n all: NetworkAddress[]\n userVisible: NetworkAddress[]\n primaryRemoteUrl?: string\n}\n\nexport function resolveNetworkAddresses(args: {\n host: string\n protocol: \"http\" | \"https\"\n port: number\n}): NetworkAddress[] {\n const { protocol, port } = args\n const host = normalizeNetworkHost(args.host)\n const interfaces = os.networkInterfaces()\n const seen = new Set<string>()\n const results: NetworkAddress[] = []\n\n const addAddress = (ip: string, scope: NetworkAddress[\"scope\"]) => {\n const normalizedIp = normalizeNetworkHost(ip)\n const ipVersion = isIP(normalizedIp)\n if (!ipVersion || isWildcardHost(normalizedIp) || (ipVersion === 6 && isLinkLocalIPv6(normalizedIp))) return\n const family = ipVersion === 6 ? \"ipv6\" : \"ipv4\"\n const key = `${family}-${normalizedIp}`\n if (seen.has(key)) return\n seen.add(key)\n results.push({ ip: normalizedIp, family, scope, remoteUrl: `${protocol}://${formatHostForUrl(normalizedIp)}:${port}` })\n }\n\n if (isWildcardHost(host)) {\n const wildcardVersion = isIP(host)\n for (const entries of Object.values(interfaces)) {\n if (!entries) continue\n for (const entry of entries) {\n const entryVersion = isIP(normalizeNetworkHost(entry.address))\n if (entryVersion !== wildcardVersion && !(wildcardVersion === 6 && entryVersion === 4)) continue\n const scope: NetworkAddress[\"scope\"] = entry.internal ? \"loopback\" : \"external\"\n addAddress(entry.address, scope)\n }\n }\n } else if (isIP(host)) {\n addAddress(host, isLoopbackHost(host) ? \"loopback\" : \"external\")\n }\n\n const scopeWeight: Record<NetworkAddress[\"scope\"], number> = { external: 0, internal: 1, loopback: 2 }\n\n return results.sort((a, b) => {\n const scopeDelta = scopeWeight[a.scope] - scopeWeight[b.scope]\n if (scopeDelta !== 0) return scopeDelta\n\n return 0\n })\n}\n\nexport function resolveRemoteAddresses(args: {\n host: string\n protocol: \"http\" | \"https\"\n port: number\n}): ResolvedRemoteAddresses {\n const all = resolveNetworkAddresses(args)\n const userVisible = sortUserVisibleAddresses(all.filter((address) => address.scope === \"external\"))\n return {\n all,\n userVisible,\n primaryRemoteUrl: userVisible[0]?.remoteUrl,\n }\n}\n\nfunction sortUserVisibleAddresses(addresses: NetworkAddress[]): NetworkAddress[] {\n return [...addresses].sort((left, right) => getUserVisiblePriority(left.ip) - getUserVisiblePriority(right.ip))\n}\n\nfunction getUserVisiblePriority(ip: string): number {\n if (isPrivateIPv4(ip)) return 0\n if (isLinkLocalIPv4(ip)) return 2\n return 1\n}\n\nfunction isLinkLocalIPv4(ip: string): boolean {\n const octets = parseIPv4(ip)\n if (!octets) return false\n const [first, second] = octets\n return first === 169 && second === 254\n}\n\nfunction isPrivateIPv4(ip: string): boolean {\n const octets = parseIPv4(ip)\n if (!octets) return false\n const [first, second] = octets\n\n if (first === 10) return true\n if (first === 192 && second === 168) return true\n return first === 172 && second >= 16 && second <= 31\n}\n\nfunction parseIPv4(value: string): number[] | null {\n if (isIP(value) !== 4) return null\n return value.split(\".\").map((part) => Number(part))\n}\n\nfunction isLinkLocalIPv6(ip: string): boolean {\n const firstSegment = Number.parseInt(ip.split(\":\", 1)[0], 16)\n return Number.isInteger(firstSegment) && firstSegment >= 0xfe80 && firstSegment <= 0xfebf\n}\n"]}
@@ -0,0 +1,62 @@
1
+ import { isIP } from "node:net";
2
+ import { domainToASCII } from "node:url";
3
+ export function stripHostBrackets(host) {
4
+ const trimmed = host.trim();
5
+ return trimmed.startsWith("[") && trimmed.endsWith("]") ? trimmed.slice(1, -1) : trimmed;
6
+ }
7
+ export function normalizeNetworkHost(host) {
8
+ const value = stripHostBrackets(host).toLowerCase();
9
+ if (isIP(value) === 6) {
10
+ const canonical = canonicalIPv6(value);
11
+ const mapped = canonical?.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
12
+ if (mapped) {
13
+ const high = Number.parseInt(mapped[1], 16);
14
+ const low = Number.parseInt(mapped[2], 16);
15
+ return `${high >> 8}.${high & 0xff}.${low >> 8}.${low & 0xff}`;
16
+ }
17
+ }
18
+ if (isIP(value))
19
+ return value;
20
+ return domainToASCII(value) || value;
21
+ }
22
+ export function hasIPv6Zone(host) {
23
+ const value = stripHostBrackets(host);
24
+ return value.includes("%") && isIP(value) === 6;
25
+ }
26
+ export function stripIPv6Zone(host) {
27
+ const value = stripHostBrackets(host);
28
+ return hasIPv6Zone(value) ? value.slice(0, value.lastIndexOf("%")) : value;
29
+ }
30
+ export function isWildcardHost(host) {
31
+ const value = normalizeNetworkHost(host);
32
+ if (value === "0.0.0.0")
33
+ return true;
34
+ return isIP(value) === 6 && value.split(":").every((segment) => segment === "" || /^0+$/.test(segment));
35
+ }
36
+ export function isLoopbackHost(host) {
37
+ const value = normalizeNetworkHost(host);
38
+ if (value === "localhost")
39
+ return true;
40
+ if (isIP(value) === 4)
41
+ return value.startsWith("127.");
42
+ if (isIP(value) !== 6)
43
+ return false;
44
+ const segments = value.split(":");
45
+ const last = segments.pop();
46
+ return Boolean(last && /^0*1$/.test(last)) && segments.every((segment) => segment === "" || /^0+$/.test(segment));
47
+ }
48
+ export function formatHostForUrl(host) {
49
+ const value = normalizeNetworkHost(host);
50
+ return isIP(value) === 6 ? `[${value}]` : value;
51
+ }
52
+ function canonicalIPv6(value) {
53
+ if (hasIPv6Zone(value))
54
+ return null;
55
+ try {
56
+ return new URL(`http://[${value}]`).hostname.slice(1, -1);
57
+ }
58
+ catch {
59
+ return null;
60
+ }
61
+ }
62
+ //# sourceMappingURL=network-host.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"network-host.js","sourceRoot":"","sources":["../../src/server/network-host.ts"],"names":[],"mappings":"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","sourcesContent":["import { isIP } from \"node:net\"\nimport { domainToASCII } from \"node:url\"\n\nexport function stripHostBrackets(host: string): string {\n const trimmed = host.trim()\n return trimmed.startsWith(\"[\") && trimmed.endsWith(\"]\") ? trimmed.slice(1, -1) : trimmed\n}\n\nexport function normalizeNetworkHost(host: string): string {\n const value = stripHostBrackets(host).toLowerCase()\n if (isIP(value) === 6) {\n const canonical = canonicalIPv6(value)\n const mapped = canonical?.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/)\n if (mapped) {\n const high = Number.parseInt(mapped[1], 16)\n const low = Number.parseInt(mapped[2], 16)\n return `${high >> 8}.${high & 0xff}.${low >> 8}.${low & 0xff}`\n }\n }\n if (isIP(value)) return value\n return domainToASCII(value) || value\n}\n\nexport function hasIPv6Zone(host: string): boolean {\n const value = stripHostBrackets(host)\n return value.includes(\"%\") && isIP(value) === 6\n}\n\nexport function stripIPv6Zone(host: string): string {\n const value = stripHostBrackets(host)\n return hasIPv6Zone(value) ? value.slice(0, value.lastIndexOf(\"%\")) : value\n}\n\nexport function isWildcardHost(host: string): boolean {\n const value = normalizeNetworkHost(host)\n if (value === \"0.0.0.0\") return true\n return isIP(value) === 6 && value.split(\":\").every((segment) => segment === \"\" || /^0+$/.test(segment))\n}\n\nexport function isLoopbackHost(host: string): boolean {\n const value = normalizeNetworkHost(host)\n if (value === \"localhost\") return true\n if (isIP(value) === 4) return value.startsWith(\"127.\")\n if (isIP(value) !== 6) return false\n\n const segments = value.split(\":\")\n const last = segments.pop()\n return Boolean(last && /^0*1$/.test(last)) && segments.every((segment) => segment === \"\" || /^0+$/.test(segment))\n}\n\nexport function formatHostForUrl(host: string): string {\n const value = normalizeNetworkHost(host)\n return isIP(value) === 6 ? `[${value}]` : value\n}\n\nfunction canonicalIPv6(value: string): string | null {\n if (hasIPv6Zone(value)) return null\n try {\n return new URL(`http://[${value}]`).hostname.slice(1, -1)\n } catch {\n return null\n }\n}\n"]}
@@ -1,3 +1,4 @@
1
+ import { isLoopbackHost, isWildcardHost } from "../network-host";
1
2
  export function registerMetaRoutes(app, deps) {
2
3
  app.get("/api/meta", async () => buildMetaResponse(deps.serverMeta));
3
4
  }
@@ -8,7 +9,7 @@ function buildMetaResponse(meta) {
8
9
  ...meta,
9
10
  localPort,
10
11
  remotePort: remote?.port,
11
- listeningMode: meta.host === "0.0.0.0" || !isLoopbackHost(meta.host) ? "all" : "local",
12
+ listeningMode: isWildcardHost(meta.host) || !isLoopbackHost(meta.host) ? "all" : "local",
12
13
  };
13
14
  }
14
15
  function resolveLocalPort(meta) {
@@ -38,8 +39,5 @@ function resolveRemote(meta) {
38
39
  return null;
39
40
  }
40
41
  }
41
- function isLoopbackHost(host) {
42
- return host === "127.0.0.1" || host === "::1" || host.startsWith("127.");
43
- }
44
42
  // NetworkAddress shape is resolved in ../network-addresses
45
43
  //# sourceMappingURL=meta.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"meta.js","sourceRoot":"","sources":["../../../src/server/routes/meta.ts"],"names":[],"mappings":"AAQA,MAAM,UAAU,kBAAkB,CAAC,GAAoB,EAAE,IAAe;IACtE,GAAG,CAAC,GAAG,CAAC,WAAW,EAAE,KAAK,IAAI,EAAE,CAAC,iBAAiB,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAA;AACtE,CAAC;AAED,SAAS,iBAAiB,CAAC,IAAgB;IACzC,MAAM,SAAS,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAA;IACxC,MAAM,MAAM,GAAG,aAAa,CAAC,IAAI,CAAC,CAAA;IAElC,OAAO;QACL,GAAG,IAAI;QACP,SAAS;QACT,UAAU,EAAE,MAAM,EAAE,IAAI;QACxB,aAAa,EAAE,IAAI,CAAC,IAAI,KAAK,SAAS,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO;KACvF,CAAA;AACH,CAAC;AAED,SAAS,gBAAgB,CAAC,IAAgB;IACxC,IAAI,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,IAAI,CAAC,SAAS,GAAG,CAAC,EAAE,CAAC;QAC3D,OAAO,IAAI,CAAC,SAAS,CAAA;IACvB,CAAC;IACD,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;QACrC,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;QAChC,OAAO,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAA;IACtD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,CAAC,CAAA;IACV,CAAC;AACH,CAAC;AAED,SAAS,aAAa,CAAC,IAAgB;IACrC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;QACpB,OAAO,IAAI,CAAA;IACb,CAAC;IACD,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;QACtC,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAA;QAChE,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;QAChC,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;IAC1E,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAA;IACb,CAAC;AACH,CAAC;AAED,SAAS,cAAc,CAAC,IAAY;IAClC,OAAO,IAAI,KAAK,WAAW,IAAI,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,CAAA;AAC1E,CAAC;AAED,2DAA2D","sourcesContent":["import { FastifyInstance } from \"fastify\"\nimport { ServerMeta } from \"../../api-types\"\n \n\ninterface RouteDeps {\n serverMeta: ServerMeta\n}\n\nexport function registerMetaRoutes(app: FastifyInstance, deps: RouteDeps) {\n app.get(\"/api/meta\", async () => buildMetaResponse(deps.serverMeta))\n}\n\nfunction buildMetaResponse(meta: ServerMeta): ServerMeta {\n const localPort = resolveLocalPort(meta)\n const remote = resolveRemote(meta)\n\n return {\n ...meta,\n localPort,\n remotePort: remote?.port,\n listeningMode: meta.host === \"0.0.0.0\" || !isLoopbackHost(meta.host) ? \"all\" : \"local\",\n }\n}\n\nfunction resolveLocalPort(meta: ServerMeta): number {\n if (Number.isInteger(meta.localPort) && meta.localPort > 0) {\n return meta.localPort\n }\n try {\n const parsed = new URL(meta.localUrl)\n const port = Number(parsed.port)\n return Number.isInteger(port) && port > 0 ? port : 0\n } catch {\n return 0\n }\n}\n\nfunction resolveRemote(meta: ServerMeta): { protocol: \"http\" | \"https\"; port: number } | null {\n if (!meta.remoteUrl) {\n return null\n }\n try {\n const parsed = new URL(meta.remoteUrl)\n const protocol = parsed.protocol === \"https:\" ? \"https\" : \"http\"\n const port = Number(parsed.port)\n return { protocol, port: Number.isInteger(port) && port > 0 ? port : 0 }\n } catch {\n return null\n }\n}\n\nfunction isLoopbackHost(host: string): boolean {\n return host === \"127.0.0.1\" || host === \"::1\" || host.startsWith(\"127.\")\n}\n\n// NetworkAddress shape is resolved in ../network-addresses\n"]}
1
+ {"version":3,"file":"meta.js","sourceRoot":"","sources":["../../../src/server/routes/meta.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAOhE,MAAM,UAAU,kBAAkB,CAAC,GAAoB,EAAE,IAAe;IACtE,GAAG,CAAC,GAAG,CAAC,WAAW,EAAE,KAAK,IAAI,EAAE,CAAC,iBAAiB,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAA;AACtE,CAAC;AAED,SAAS,iBAAiB,CAAC,IAAgB;IACzC,MAAM,SAAS,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAA;IACxC,MAAM,MAAM,GAAG,aAAa,CAAC,IAAI,CAAC,CAAA;IAElC,OAAO;QACL,GAAG,IAAI;QACP,SAAS;QACT,UAAU,EAAE,MAAM,EAAE,IAAI;QACxB,aAAa,EAAE,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO;KACzF,CAAA;AACH,CAAC;AAED,SAAS,gBAAgB,CAAC,IAAgB;IACxC,IAAI,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,IAAI,CAAC,SAAS,GAAG,CAAC,EAAE,CAAC;QAC3D,OAAO,IAAI,CAAC,SAAS,CAAA;IACvB,CAAC;IACD,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;QACrC,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;QAChC,OAAO,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAA;IACtD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,CAAC,CAAA;IACV,CAAC;AACH,CAAC;AAED,SAAS,aAAa,CAAC,IAAgB;IACrC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;QACpB,OAAO,IAAI,CAAA;IACb,CAAC;IACD,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;QACtC,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAA;QAChE,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;QAChC,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;IAC1E,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAA;IACb,CAAC;AACH,CAAC;AAED,2DAA2D","sourcesContent":["import { FastifyInstance } from \"fastify\"\nimport { ServerMeta } from \"../../api-types\"\nimport { isLoopbackHost, isWildcardHost } from \"../network-host\"\n \n\ninterface RouteDeps {\n serverMeta: ServerMeta\n}\n\nexport function registerMetaRoutes(app: FastifyInstance, deps: RouteDeps) {\n app.get(\"/api/meta\", async () => buildMetaResponse(deps.serverMeta))\n}\n\nfunction buildMetaResponse(meta: ServerMeta): ServerMeta {\n const localPort = resolveLocalPort(meta)\n const remote = resolveRemote(meta)\n\n return {\n ...meta,\n localPort,\n remotePort: remote?.port,\n listeningMode: isWildcardHost(meta.host) || !isLoopbackHost(meta.host) ? \"all\" : \"local\",\n }\n}\n\nfunction resolveLocalPort(meta: ServerMeta): number {\n if (Number.isInteger(meta.localPort) && meta.localPort > 0) {\n return meta.localPort\n }\n try {\n const parsed = new URL(meta.localUrl)\n const port = Number(parsed.port)\n return Number.isInteger(port) && port > 0 ? port : 0\n } catch {\n return 0\n }\n}\n\nfunction resolveRemote(meta: ServerMeta): { protocol: \"http\" | \"https\"; port: number } | null {\n if (!meta.remoteUrl) {\n return null\n }\n try {\n const parsed = new URL(meta.remoteUrl)\n const protocol = parsed.protocol === \"https:\" ? \"https\" : \"http\"\n const port = Number(parsed.port)\n return { protocol, port: Number.isInteger(port) && port > 0 ? port : 0 }\n } catch {\n return null\n }\n}\n\n// NetworkAddress shape is resolved in ../network-addresses\n"]}
@@ -0,0 +1,45 @@
1
+ import { messageTargetSchema, PRUNING_RPC_ID, prunePreviewSchema, pruneRequestSchema, pruneResultSchema } from "../../opencode/session-pruning/contract";
2
+ export function registerSessionPruningRoutes(app, deps) {
3
+ for (const method of ["preview", "prune"]) {
4
+ app.post(`/api/workspaces/:id/session-pruning/${method}`, { bodyLimit: 1024 * 1024 }, async (request, reply) => {
5
+ const input = (method === "prune" ? pruneRequestSchema : messageTargetSchema).safeParse(request.body);
6
+ if (!input.success)
7
+ return reply.code(400).send({ error: "Invalid pruning request" });
8
+ const client = await deps.workspaceManager.getSharedServiceClient();
9
+ const session = await client.session.get({ sessionID: input.data.sessionID });
10
+ if (!await deps.workspaceManager.ownsLocation(request.params.id, session.location)) {
11
+ return reply.code(403).send({ error: "Session does not belong to workspace" });
12
+ }
13
+ const identity = await deps.workspaceManager.getWorktreeIdentityForPath(request.params.id, session.location.directory);
14
+ if (!identity)
15
+ return reply.code(403).send({ error: "Session does not belong to workspace" });
16
+ const release = deps.worktreeDeletionFence.enter([identity]);
17
+ if (!release)
18
+ return reply.code(409).send({ error: "Worktree deletion is in progress" });
19
+ try {
20
+ // Narrow broker only: callers cannot choose RPC ID, method, location,
21
+ // database filename, SQL, or replacement content. Generic RPC stays blocked.
22
+ const result = await client.rpc.call({
23
+ rpcID: PRUNING_RPC_ID, method, input: input.data, location: { directory: session.location.directory },
24
+ }, { signal: AbortSignal.timeout(15_000) });
25
+ const output = (method === "prune" ? pruneResultSchema : prunePreviewSchema).safeParse(result.output);
26
+ if (!output.success)
27
+ return { status: "blocked", reason: "unavailable" };
28
+ if (output.data.status === "pruned" && (output.data.messageID !== input.data.messageID
29
+ || !("indexes" in input.data) || !Array.isArray(input.data.indexes) || output.data.removedCount !== input.data.indexes.length)) {
30
+ return { status: "blocked", reason: "unavailable" };
31
+ }
32
+ return output.data;
33
+ }
34
+ catch {
35
+ // Missing plugin, incompatible runtime, timeout: no legacy API fallback
36
+ // and no local-only deletion presented as a successful database write.
37
+ return { status: "blocked", reason: "unavailable" };
38
+ }
39
+ finally {
40
+ release();
41
+ }
42
+ });
43
+ }
44
+ }
45
+ //# sourceMappingURL=session-pruning.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-pruning.js","sourceRoot":"","sources":["../../../src/server/routes/session-pruning.ts"],"names":[],"mappings":"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","sourcesContent":["import type { FastifyInstance } from \"fastify\"\nimport { messageTargetSchema, PRUNING_RPC_ID, prunePreviewSchema, pruneRequestSchema, pruneResultSchema } from \"../../opencode/session-pruning/contract\"\nimport type { WorkspaceManager } from \"../../workspaces/manager\"\nimport type { WorktreeDeletionFence } from \"../../workspaces/worktree-session-evacuation\"\n\ninterface RouteDeps {\n workspaceManager: Pick<WorkspaceManager, \"getSharedServiceClient\" | \"ownsLocation\" | \"getWorktreeIdentityForPath\">\n worktreeDeletionFence: WorktreeDeletionFence\n}\n\nexport function registerSessionPruningRoutes(app: FastifyInstance, deps: RouteDeps): void {\n for (const method of [\"preview\", \"prune\"] as const) {\n app.post<{ Params: { id: string } }>(`/api/workspaces/:id/session-pruning/${method}`, { bodyLimit: 1024 * 1024 }, async (request, reply) => {\n const input = (method === \"prune\" ? pruneRequestSchema : messageTargetSchema).safeParse(request.body)\n if (!input.success) return reply.code(400).send({ error: \"Invalid pruning request\" })\n const client = await deps.workspaceManager.getSharedServiceClient()\n const session = await client.session.get({ sessionID: input.data.sessionID })\n if (!await deps.workspaceManager.ownsLocation(request.params.id, session.location)) {\n return reply.code(403).send({ error: \"Session does not belong to workspace\" })\n }\n const identity = await deps.workspaceManager.getWorktreeIdentityForPath(request.params.id, session.location.directory)\n if (!identity) return reply.code(403).send({ error: \"Session does not belong to workspace\" })\n const release = deps.worktreeDeletionFence.enter([identity])\n if (!release) return reply.code(409).send({ error: \"Worktree deletion is in progress\" })\n try {\n // Narrow broker only: callers cannot choose RPC ID, method, location,\n // database filename, SQL, or replacement content. Generic RPC stays blocked.\n const result = await client.rpc.call({\n rpcID: PRUNING_RPC_ID, method, input: input.data, location: { directory: session.location.directory },\n }, { signal: AbortSignal.timeout(15_000) })\n const output = (method === \"prune\" ? pruneResultSchema : prunePreviewSchema).safeParse(result.output)\n if (!output.success) return { status: \"blocked\", reason: \"unavailable\" }\n if (output.data.status === \"pruned\" && (output.data.messageID !== input.data.messageID\n || !(\"indexes\" in input.data) || !Array.isArray(input.data.indexes) || output.data.removedCount !== input.data.indexes.length)) {\n return { status: \"blocked\", reason: \"unavailable\" }\n }\n return output.data\n } catch {\n // Missing plugin, incompatible runtime, timeout: no legacy API fallback\n // and no local-only deletion presented as a successful database write.\n return { status: \"blocked\", reason: \"unavailable\" }\n } finally { release() }\n })\n }\n}\n"]}
@@ -0,0 +1,98 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import Fastify from "fastify";
4
+ import { registerSessionPruningRoutes } from "./session-pruning";
5
+ import { WorktreeDeletionFence } from "../../workspaces/worktree-session-evacuation";
6
+ const payload = { sessionID: "s", messageID: "m", revision: "a".repeat(64), indexes: [0] };
7
+ const url = "/api/workspaces/w/session-pruning/prune";
8
+ function fixture(options = {}) {
9
+ const calls = [];
10
+ const app = Fastify();
11
+ const fence = new WorktreeDeletionFence();
12
+ registerSessionPruningRoutes(app, { worktreeDeletionFence: fence, workspaceManager: {
13
+ getSharedServiceClient: async () => ({
14
+ session: { get: async () => ({ location: { directory: "/owned/worktree" } }) },
15
+ rpc: { call: async (input, opts) => {
16
+ assert(opts.signal instanceof AbortSignal);
17
+ calls.push(input);
18
+ if (options.fail)
19
+ throw new Error("plugin unavailable");
20
+ return { output: options.output ?? { status: "blocked", reason: "maintenance_required" } };
21
+ } },
22
+ }),
23
+ ownsLocation: async () => options.owned !== false,
24
+ getWorktreeIdentityForPath: async () => "/owned/worktree",
25
+ } });
26
+ return { app, calls, fence };
27
+ }
28
+ test("broker pins RPC, method and native session location", async () => {
29
+ const { app, calls } = fixture();
30
+ try {
31
+ const response = await app.inject({ method: "POST", url, payload });
32
+ assert.equal(response.statusCode, 200);
33
+ assert.deepEqual(response.json(), { status: "blocked", reason: "maintenance_required" });
34
+ assert.deepEqual(calls, [{ rpcID: "codenomad.session-pruning", method: "prune", input: payload, location: { directory: "/owned/worktree" } }]);
35
+ }
36
+ finally {
37
+ await app.close();
38
+ }
39
+ });
40
+ test("rejects unowned sessions before any RPC", async () => {
41
+ const { app, calls } = fixture({ owned: false });
42
+ try {
43
+ assert.equal((await app.inject({ method: "POST", url, payload })).statusCode, 403);
44
+ assert.equal(calls.length, 0);
45
+ }
46
+ finally {
47
+ await app.close();
48
+ }
49
+ });
50
+ test("rejects caller-selected location, RPC, database and replacement content", async () => {
51
+ const { app, calls } = fixture();
52
+ try {
53
+ for (const extra of [{ location: { directory: "/other" } }, { rpcID: "other" }, { databasePath: "live.db" }, { content: [] }]) {
54
+ assert.equal((await app.inject({ method: "POST", url, payload: { ...payload, ...extra } })).statusCode, 400);
55
+ }
56
+ assert.equal(calls.length, 0);
57
+ }
58
+ finally {
59
+ await app.close();
60
+ }
61
+ });
62
+ test("does not bypass worktree deletion fence", async () => {
63
+ const { app, calls, fence } = fixture();
64
+ try {
65
+ await fence.run("/owned/worktree", ["/owned/worktree"], async () => {
66
+ assert.equal((await app.inject({ method: "POST", url, payload })).statusCode, 409);
67
+ });
68
+ assert.equal(calls.length, 0);
69
+ }
70
+ finally {
71
+ await app.close();
72
+ }
73
+ });
74
+ test("missing plugin, malformed response, wrong message or count never report success", async () => {
75
+ for (const options of [{ fail: true }, { output: {} }, { output: { status: "pruned", messageID: "other", revision: "a".repeat(64), removedCount: 1 } }, { output: { status: "pruned", messageID: "m", revision: "a".repeat(64), removedCount: 2 } }]) {
76
+ const { app, fence } = fixture(options);
77
+ try {
78
+ const response = await app.inject({ method: "POST", url, payload });
79
+ assert.deepEqual(response.json(), { status: "blocked", reason: "unavailable" });
80
+ await fence.run("/owned/worktree", ["/owned/worktree"], async () => { });
81
+ }
82
+ finally {
83
+ await app.close();
84
+ }
85
+ }
86
+ });
87
+ test("preview accepts IDs only and uses a distinct fixed RPC method", async () => {
88
+ const { app, calls } = fixture({ output: { status: "preview", revision: "a".repeat(64), liveMutation: false, parts: [] } });
89
+ try {
90
+ const response = await app.inject({ method: "POST", url: url.replace(/prune$/, "preview"), payload: { sessionID: "s", messageID: "m" } });
91
+ assert.equal(response.json().status, "preview");
92
+ assert.equal(calls[0].method, "preview");
93
+ }
94
+ finally {
95
+ await app.close();
96
+ }
97
+ });
98
+ //# sourceMappingURL=session-pruning.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-pruning.test.js","sourceRoot":"","sources":["../../../src/server/routes/session-pruning.test.ts"],"names":[],"mappings":"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","sourcesContent":["import assert from \"node:assert/strict\"\nimport { test } from \"node:test\"\nimport Fastify from \"fastify\"\nimport { registerSessionPruningRoutes } from \"./session-pruning\"\nimport { WorktreeDeletionFence } from \"../../workspaces/worktree-session-evacuation\"\n\nconst payload = { sessionID: \"s\", messageID: \"m\", revision: \"a\".repeat(64), indexes: [0] }\nconst url = \"/api/workspaces/w/session-pruning/prune\"\nfunction fixture(options: { owned?: boolean; output?: unknown; fail?: boolean } = {}) {\n const calls: any[] = []\n const app = Fastify()\n const fence = new WorktreeDeletionFence()\n registerSessionPruningRoutes(app, { worktreeDeletionFence: fence, workspaceManager: {\n getSharedServiceClient: async () => ({\n session: { get: async () => ({ location: { directory: \"/owned/worktree\" } }) },\n rpc: { call: async (input: unknown, opts: any) => {\n assert(opts.signal instanceof AbortSignal)\n calls.push(input)\n if (options.fail) throw new Error(\"plugin unavailable\")\n return { output: options.output ?? { status: \"blocked\", reason: \"maintenance_required\" } }\n } },\n }) as any,\n ownsLocation: async () => options.owned !== false,\n getWorktreeIdentityForPath: async () => \"/owned/worktree\",\n } })\n return { app, calls, fence }\n}\n\ntest(\"broker pins RPC, method and native session location\", async () => {\n const { app, calls } = fixture()\n try {\n const response = await app.inject({ method: \"POST\", url, payload })\n assert.equal(response.statusCode, 200)\n assert.deepEqual(response.json(), { status: \"blocked\", reason: \"maintenance_required\" })\n assert.deepEqual(calls, [{ rpcID: \"codenomad.session-pruning\", method: \"prune\", input: payload, location: { directory: \"/owned/worktree\" } }])\n } finally { await app.close() }\n})\n\ntest(\"rejects unowned sessions before any RPC\", async () => {\n const { app, calls } = fixture({ owned: false })\n try {\n assert.equal((await app.inject({ method: \"POST\", url, payload })).statusCode, 403)\n assert.equal(calls.length, 0)\n } finally { await app.close() }\n})\n\ntest(\"rejects caller-selected location, RPC, database and replacement content\", async () => {\n const { app, calls } = fixture()\n try {\n for (const extra of [{ location: { directory: \"/other\" } }, { rpcID: \"other\" }, { databasePath: \"live.db\" }, { content: [] }]) {\n assert.equal((await app.inject({ method: \"POST\", url, payload: { ...payload, ...extra } })).statusCode, 400)\n }\n assert.equal(calls.length, 0)\n } finally { await app.close() }\n})\n\ntest(\"does not bypass worktree deletion fence\", async () => {\n const { app, calls, fence } = fixture()\n try {\n await fence.run(\"/owned/worktree\", [\"/owned/worktree\"], async () => {\n assert.equal((await app.inject({ method: \"POST\", url, payload })).statusCode, 409)\n })\n assert.equal(calls.length, 0)\n } finally { await app.close() }\n})\n\ntest(\"missing plugin, malformed response, wrong message or count never report success\", async () => {\n for (const options of [{ fail: true }, { output: {} }, { output: { status: \"pruned\", messageID: \"other\", revision: \"a\".repeat(64), removedCount: 1 } }, { output: { status: \"pruned\", messageID: \"m\", revision: \"a\".repeat(64), removedCount: 2 } }]) {\n const { app, fence } = fixture(options)\n try {\n const response = await app.inject({ method: \"POST\", url, payload })\n assert.deepEqual(response.json(), { status: \"blocked\", reason: \"unavailable\" })\n await fence.run(\"/owned/worktree\", [\"/owned/worktree\"], async () => {})\n } finally { await app.close() }\n }\n})\n\ntest(\"preview accepts IDs only and uses a distinct fixed RPC method\", async () => {\n const { app, calls } = fixture({ output: { status: \"preview\", revision: \"a\".repeat(64), liveMutation: false, parts: [] } })\n try {\n const response = await app.inject({ method: \"POST\", url: url.replace(/prune$/, \"preview\"), payload: { sessionID: \"s\", messageID: \"m\" } })\n assert.equal(response.json().status, \"preview\")\n assert.equal(calls[0].method, \"preview\")\n } finally { await app.close() }\n})\n"]}
@@ -2,6 +2,8 @@ import crypto from "crypto";
2
2
  import fs from "fs";
3
3
  import path from "path";
4
4
  import { createRequire } from "module";
5
+ import { isIP } from "node:net";
6
+ import { isWildcardHost, normalizeNetworkHost, stripIPv6Zone } from "./network-host";
5
7
  const require = createRequire(import.meta.url);
6
8
  function loadForge() {
7
9
  // node-forge is CJS in many installs; require keeps this compatible with our ESM output.
@@ -39,12 +41,12 @@ function ensureGeneratedTls(args) {
39
41
  if (!fs.existsSync(certPath))
40
42
  return true;
41
43
  const pem = fs.readFileSync(certPath, "utf-8");
42
- const x509 = new crypto.X509Certificate(pem);
43
- const validToMs = Date.parse(x509.validTo);
44
+ const certificate = new crypto.X509Certificate(pem);
45
+ const validToMs = Date.parse(certificate.validTo);
44
46
  if (!Number.isFinite(validToMs))
45
47
  return true;
46
48
  const rotateAt = validToMs - ROTATE_IF_EXPIRES_WITHIN_DAYS * 24 * 60 * 60 * 1000;
47
- return Date.now() >= rotateAt;
49
+ return Date.now() >= rotateAt || !certificateCoversRequiredSans(certificate, args.host, args.tlsSANs);
48
50
  }
49
51
  catch {
50
52
  return true;
@@ -164,44 +166,73 @@ function generateServerCertificate(args) {
164
166
  };
165
167
  }
166
168
  function pickCommonName(host) {
167
- if (!host || host === "0.0.0.0") {
169
+ const normalizedHost = normalizeCertificateHost(host);
170
+ if (!normalizedHost || isWildcardHost(normalizedHost)) {
168
171
  return "localhost";
169
172
  }
170
- if (host === "127.0.0.1") {
173
+ if (normalizedHost === "127.0.0.1") {
171
174
  return "localhost";
172
175
  }
173
- return host;
176
+ return normalizedHost;
174
177
  }
175
178
  function buildSubjectAltNames(host, tlsSANs) {
179
+ const { dns, ips } = resolveRequiredSubjectAltNames(host, tlsSANs);
180
+ const altNames = [];
181
+ // 2 = DNS, 7 = IP
182
+ for (const name of dns) {
183
+ altNames.push({ type: 2, value: name });
184
+ }
185
+ for (const ip of ips) {
186
+ altNames.push({ type: 7, ip });
187
+ }
188
+ return altNames;
189
+ }
190
+ function resolveRequiredSubjectAltNames(host, tlsSANs) {
176
191
  const dns = new Set();
177
192
  const ips = new Set();
178
193
  dns.add("localhost");
179
194
  ips.add("127.0.0.1");
180
- if (host && host !== "0.0.0.0") {
181
- if (isIPv4(host)) {
182
- ips.add(host);
195
+ const normalizedHost = normalizeCertificateHost(host);
196
+ if (isWildcardHost(normalizedHost) && isIP(normalizedHost) === 6) {
197
+ ips.add("::1");
198
+ }
199
+ else if (normalizedHost) {
200
+ if (isIP(normalizedHost)) {
201
+ ips.add(normalizedHost);
183
202
  }
184
203
  else {
185
- dns.add(host);
204
+ dns.add(normalizedHost);
186
205
  }
187
206
  }
188
207
  for (const token of splitList(tlsSANs)) {
189
- if (isIPv4(token)) {
190
- ips.add(token);
208
+ const normalizedToken = normalizeCertificateHost(token);
209
+ if (isIP(normalizedToken)) {
210
+ ips.add(normalizedToken);
191
211
  }
192
- else if (token) {
193
- dns.add(token);
212
+ else if (normalizedToken) {
213
+ dns.add(normalizedToken);
194
214
  }
195
215
  }
196
- const altNames = [];
197
- // 2 = DNS, 7 = IP
198
- for (const name of Array.from(dns)) {
199
- altNames.push({ type: 2, value: name });
216
+ return { dns, ips };
217
+ }
218
+ function normalizeCertificateHost(host) {
219
+ return normalizeNetworkHost(stripIPv6Zone(host));
220
+ }
221
+ function certificateCoversRequiredSans(certificate, host, tlsSANs) {
222
+ const { dns, ips } = resolveRequiredSubjectAltNames(host, tlsSANs);
223
+ const existingDns = new Set((certificate.subjectAltName ?? "")
224
+ .split(/,\s*/)
225
+ .filter((entry) => entry.startsWith("DNS:"))
226
+ .map((entry) => entry.slice(4).toLowerCase()));
227
+ for (const name of dns) {
228
+ if (!existingDns.has(name.toLowerCase()))
229
+ return false;
200
230
  }
201
- for (const ip of Array.from(ips)) {
202
- altNames.push({ type: 7, ip });
231
+ for (const ip of ips) {
232
+ if (!certificate.checkIP(ip))
233
+ return false;
203
234
  }
204
- return altNames;
235
+ return true;
205
236
  }
206
237
  function splitList(input) {
207
238
  if (!input)
@@ -211,15 +242,4 @@ function splitList(input) {
211
242
  .map((part) => part.trim())
212
243
  .filter(Boolean);
213
244
  }
214
- function isIPv4(value) {
215
- const parts = value.split(".");
216
- if (parts.length !== 4)
217
- return false;
218
- return parts.every((part) => {
219
- if (!/^[0-9]+$/.test(part))
220
- return false;
221
- const num = Number(part);
222
- return Number.isInteger(num) && num >= 0 && num <= 255;
223
- });
224
- }
225
245
  //# sourceMappingURL=tls.js.map