peachsim 0.1.1481 → 0.1.1619

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 (292) hide show
  1. package/README.md +56 -13
  2. package/cli/app-catalog.ts +7 -5
  3. package/cli/app-config.ts +47 -8
  4. package/cli/app-fonts.ts +84 -49
  5. package/cli/app-stubs.ts +433 -0
  6. package/cli/bin.ts +15 -2
  7. package/cli/bridge-flow-runner.ts +383 -123
  8. package/cli/cloud-client.ts +26 -25
  9. package/cli/cloud-test-tunnel.ts +2 -0
  10. package/cli/command-registry.ts +2 -1
  11. package/cli/commands/agent-indicator-mode.ts +39 -0
  12. package/cli/commands/box/bundle-plane.ts +4 -0
  13. package/cli/commands/box.ts +16 -23
  14. package/cli/commands/control.ts +221 -126
  15. package/cli/commands/daemon.ts +99 -24
  16. package/cli/commands/debug.ts +31 -3
  17. package/cli/commands/detect.ts +1 -1
  18. package/cli/commands/device.ts +6 -8
  19. package/cli/commands/diagnose.ts +2 -0
  20. package/cli/commands/do-chain.ts +2 -2
  21. package/cli/commands/doctor.ts +1030 -0
  22. package/cli/commands/flow.ts +447 -406
  23. package/cli/commands/inspect/actions.ts +2 -0
  24. package/cli/commands/inspect/core.ts +138 -46
  25. package/cli/commands/inspect/env.ts +10 -0
  26. package/cli/commands/inspect/list.ts +25 -8
  27. package/cli/commands/inspect/memory.ts +9 -0
  28. package/cli/commands/inspect/settling.ts +219 -66
  29. package/cli/commands/inspect/shared.ts +53 -12
  30. package/cli/commands/inspect/wait-event.ts +0 -1
  31. package/cli/commands/inspect/wait.ts +2 -2
  32. package/cli/commands/inspect.ts +51 -16
  33. package/cli/commands/install-desktop.ts +3 -2
  34. package/cli/commands/kotlin.ts +81 -22
  35. package/cli/commands/login.ts +1 -1
  36. package/cli/commands/maestro-compare.ts +41 -5
  37. package/cli/commands/maestro-generate.ts +11 -7
  38. package/cli/commands/maestro.ts +43 -15
  39. package/cli/commands/mode.ts +13 -1
  40. package/cli/commands/no-bridge-hint.ts +26 -3
  41. package/cli/commands/perf-record.ts +296 -0
  42. package/cli/commands/perf.ts +6 -1
  43. package/cli/commands/platform.ts +15 -4
  44. package/cli/commands/preview.ts +1 -1
  45. package/cli/commands/record.ts +37 -6
  46. package/cli/commands/report-issue.ts +2 -1
  47. package/cli/commands/runtime.ts +9 -11
  48. package/cli/commands/screenshots.ts +4 -3
  49. package/cli/commands/serve.ts +30 -2
  50. package/cli/commands/setup.ts +32 -7
  51. package/cli/commands/skills.ts +16 -43
  52. package/cli/commands/swift.ts +22 -3
  53. package/cli/commands/upgrade.ts +7 -6
  54. package/cli/commands/upload.ts +118 -69
  55. package/cli/commands/window.ts +449 -0
  56. package/cli/current-sim.ts +1 -2
  57. package/cli/desktop-companion.ts +84 -10
  58. package/cli/device-option.ts +8 -0
  59. package/cli/drivers/desktop.ts +3 -1
  60. package/cli/drivers/playwright-sim-host.ts +30 -11
  61. package/cli/drivers/playwright.ts +1 -4
  62. package/cli/flow-export.ts +2 -2
  63. package/cli/flow-file.ts +19 -18
  64. package/cli/internal-child.ts +7 -6
  65. package/cli/maestro-compare.ts +28 -6
  66. package/cli/maestro-js.ts +2 -1
  67. package/cli/main.ts +25 -4
  68. package/cli/material-symbol-fonts.ts +171 -0
  69. package/cli/outbound-endpoints.ts +132 -54
  70. package/cli/parse-args.ts +7 -0
  71. package/cli/preview-capture.ts +12 -0
  72. package/cli/privacy.ts +1 -1
  73. package/cli/run-registry.ts +5 -2
  74. package/cli/runtime-compat.ts +16 -0
  75. package/cli/telemetry.ts +6 -3
  76. package/cli/ui.ts +5 -0
  77. package/cli/ws-bridge.ts +121 -62
  78. package/detox/attempt-events.cjs +762 -0
  79. package/detox/attempt-events.d.cts +106 -0
  80. package/detox/gestures.ts +115 -2
  81. package/detox/index.ts +762 -426
  82. package/detox/jest-environment.cjs +86 -0
  83. package/detox/jest-preset.cjs +3 -1
  84. package/detox/jest-setup-after-env.cjs +1 -0
  85. package/detox/matchers.ts +3 -6
  86. package/detox/motion-change.cjs +259 -0
  87. package/detox/motion-change.d.cts +73 -0
  88. package/detox/proof-frame.cjs +83 -9
  89. package/dist-lib/agent-daemon-client.cjs +1 -1
  90. package/dist-lib/agent-events.cjs +1 -1
  91. package/dist-lib/attached-projects.cjs +1 -1
  92. package/dist-lib/auth/shared-session.cjs +15 -5
  93. package/dist-lib/backend-origin.cjs +1 -1
  94. package/dist-lib/beta.cjs +1 -1
  95. package/dist-lib/beta.mjs +1 -1
  96. package/dist-lib/bridge-constants.cjs +3 -1
  97. package/dist-lib/bridge-contract-input.cjs +1 -1
  98. package/dist-lib/bridge-contract-input.mjs +1 -1
  99. package/dist-lib/bridge-contract.cjs +1 -1
  100. package/dist-lib/bridge-contract.mjs +1 -1
  101. package/dist-lib/bundle-channel.cjs +5950 -1775
  102. package/dist-lib/capture-contract.cjs +1 -1
  103. package/dist-lib/capture-contract.mjs +1 -1
  104. package/dist-lib/cloud-contract.cjs +6 -1
  105. package/dist-lib/cloud-contract.mjs +5 -1
  106. package/dist-lib/cloud-test-args.cjs +1 -1
  107. package/dist-lib/cloud-test-args.mjs +1 -1
  108. package/dist-lib/cloud.cjs +1 -1
  109. package/dist-lib/cloud.mjs +1 -1
  110. package/dist-lib/config.cjs +34 -3
  111. package/dist-lib/config.mjs +33 -3
  112. package/dist-lib/detox/index.cjs +3443 -564
  113. package/dist-lib/detox/jest-environment.cjs +86 -0
  114. package/dist-lib/detox/jest-preset.cjs +3 -1
  115. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  116. package/dist-lib/expo-swift-bundle.cjs +5518 -1446
  117. package/dist-lib/expo-swift-metadata.cjs +21 -13
  118. package/dist-lib/expo-swift-metadata.mjs +21 -13
  119. package/dist-lib/home-paths.cjs +1 -1
  120. package/dist-lib/host/bridge-host.cjs +21728 -16808
  121. package/dist-lib/host/fetch-proxy-handler.cjs +177 -102
  122. package/dist-lib/host/fetch-proxy-overrides.cjs +84 -4
  123. package/dist-lib/host/fetch-proxy-overrides.mjs +74 -3
  124. package/dist-lib/host/replacement-module-handler.cjs +16 -15
  125. package/dist-lib/host/websocket-proxy.cjs +243 -6
  126. package/dist-lib/index.cjs +6063 -4113
  127. package/dist-lib/jump-to-source-babel.cjs +73 -35
  128. package/dist-lib/jump-to-source-native.cjs +118 -10
  129. package/dist-lib/kotlin-host.cjs +264 -121
  130. package/dist-lib/menu.cjs +1 -1
  131. package/dist-lib/menu.mjs +1 -1
  132. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  133. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  134. package/dist-lib/metro-production-bundle.cjs +147 -99
  135. package/dist-lib/metro-production-bundle.mjs +142 -99
  136. package/dist-lib/metro.cjs +4249 -360
  137. package/dist-lib/native-app-identity-project.cjs +318 -131
  138. package/dist-lib/native-app-identity-project.mjs +317 -131
  139. package/dist-lib/native-app-identity.cjs +322 -33
  140. package/dist-lib/native-app-identity.mjs +319 -32
  141. package/dist-lib/native-firebase-metadata.cjs +1 -1
  142. package/dist-lib/native-firebase-metadata.mjs +1 -1
  143. package/dist-lib/plist.cjs +1 -1
  144. package/dist-lib/plist.mjs +1 -1
  145. package/dist-lib/profiles.cjs +1 -1
  146. package/dist-lib/public-brand.cjs +1 -1
  147. package/dist-lib/react-native-host-modules.cjs +11 -1
  148. package/dist-lib/react-native-host-modules.mjs +11 -1
  149. package/dist-lib/scripts/dev-server-scanner.cjs +127 -42
  150. package/dist-lib/sdk.cjs +311 -103
  151. package/dist-lib/sdk.mjs +311 -103
  152. package/dist-lib/skills.cjs +18344 -11121
  153. package/dist-lib/swift-compile.cjs +3291 -544
  154. package/dist-lib/swift-compile.mjs +3227 -478
  155. package/dist-lib/swift-package.cjs +25 -3
  156. package/dist-lib/swift-package.mjs +22 -2
  157. package/dist-lib/swift-submit.cjs +2 -2
  158. package/dist-lib/swift-submit.mjs +2 -2
  159. package/dist-lib/swift-toolchain.cjs +1 -1
  160. package/dist-lib/swift.cjs +1 -1
  161. package/dist-lib/vite-swift.cjs +5523 -1428
  162. package/dist-lib/vite.cjs +251 -60
  163. package/dist-lib/worklet-executable.cjs +18 -4
  164. package/dist-lib/worklet-executable.mjs +18 -4
  165. package/install.sh +8 -3
  166. package/kotlin/AndroidViewSources/android/graphics/Canvas.kt +2 -0
  167. package/kotlin/AndroidViewSources/android/graphics/Paint.kt +18 -0
  168. package/kotlin/AndroidViewSources/dev/peach/app/JavaFormat.kt +16 -8
  169. package/kotlin/AppSources/android/view/Window.kt +4 -1
  170. package/kotlin/AppSources/android/window/BackEvent.kt +24 -0
  171. package/kotlin/AppSources/android/window/OnBackAnimationCallback.kt +7 -0
  172. package/kotlin/AppSources/android/window/OnBackInvokedDispatcher.kt +37 -1
  173. package/kotlin/AppSources/androidx/activity/BackEventCompat.kt +5 -0
  174. package/kotlin/AppSources/androidx/activity/OnBackPressedDispatcher.kt +8 -1
  175. package/kotlin/AppSources/dev/peach/app/PeachApp.kt +14 -0
  176. package/kotlin/CompilerSources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar +1 -0
  177. package/kotlin/CompilerSources/PeachJniIncoming.kt +243 -0
  178. package/kotlin/CompilerSources/PeachJniPlugin.kt +116 -0
  179. package/kotlin/ComposeSources/PeachComposeHost.kt +58 -32
  180. package/kotlin/ComposeSources/PeachMagnifier.kt +185 -0
  181. package/kotlin/ExpoComposeSources/PeachAndroidView.kt +16 -0
  182. package/kotlin/JniPlatformSources/androidx/annotation/CallSuper.kt +5 -0
  183. package/kotlin/JniPlatformSources/com/facebook/proguard/annotations/DoNotStrip.kt +5 -0
  184. package/kotlin/JniPlatformSources/dalvik/annotation/optimization/FastNative.kt +5 -0
  185. package/kotlin/JniPlatformSources/kotlin/Synchronized.kt +6 -0
  186. package/kotlin/JniPlatformSources/kotlin/jvm/JvmField.kt +5 -0
  187. package/kotlin/JniPlatformSources/kotlin/jvm/JvmStatic.kt +5 -0
  188. package/kotlin/JniSources/com/facebook/jni/HybridData.kt +68 -0
  189. package/kotlin/JniSources/dev/peach/jni/IncomingBindings.kt +157 -0
  190. package/kotlin/JniSources/dev/peach/jni/NativeBindings.kt +79 -0
  191. package/kotlin/JniTransportSources/dev/peach/jni/integration/JniTransport.kt +91 -0
  192. package/kotlin/README.md +18 -0
  193. package/kotlin/Sources/java/util/IllegalFormatCodePointException.kt +10 -0
  194. package/kotlin/Sources/java/util/IllegalFormatException.kt +5 -0
  195. package/kotlin/fixtures/counter/CounterApp.native.tsx +30 -3
  196. package/kotlin/fixtures/counter/CounterScreen.d.kt.ts +1 -0
  197. package/kotlin/fixtures/counter/CounterScreen.kt +2 -1
  198. package/kotlin/fixtures/counter/package.json +15 -2
  199. package/kotlin/fixtures/counter/vite.config.ts +11 -1
  200. package/kotlin/fork/NOTICE +1 -0
  201. package/kotlin/fork/README.md +5 -4
  202. package/kotlin/fork/compose-worker-globals.patch +629 -0
  203. package/kotlin/fork/fork.json +1 -1
  204. package/kotlin/toolchain.json +3 -3
  205. package/package.json +1 -1
  206. package/scripts/dev-server-scanner.ts +166 -81
  207. package/skills/contrast/SKILL.md +4 -4
  208. package/skills/peach-debug/SKILL.md +1 -1
  209. package/skills/peach-perf/SKILL.md +25 -0
  210. package/skills/peach-setup/SKILL.md +47 -13
  211. package/skills/peach-test/SKILL.md +2 -3
  212. package/src/agent-identity.ts +18 -0
  213. package/src/assetCatalogContract.ts +31 -3
  214. package/src/auth/shared-session.ts +3 -2
  215. package/src/bridge-constants.ts +1 -1
  216. package/src/bridge-contract.ts +5 -0
  217. package/src/bundle-channel.mjs +38 -7
  218. package/src/bundleGlobalFooter.ts +70 -20
  219. package/src/cloud-contract.ts +11 -0
  220. package/src/config.ts +47 -1
  221. package/src/configured-bundle-producer.ts +4 -1
  222. package/src/desktop-host-delivery.ts +4 -3
  223. package/src/expo-kotlin-host.ts +7 -2
  224. package/src/guest-bundle.ts +15 -8
  225. package/src/host/agent-host.ts +2 -4
  226. package/src/host/bridge-host.ts +260 -119
  227. package/src/host/fetch-proxy-handler.ts +237 -146
  228. package/src/host/fetch-proxy-overrides.ts +118 -0
  229. package/src/host/fetch-proxy-transport.ts +77 -63
  230. package/src/host/lease-owner.test.ts +18 -1
  231. package/src/host/lease-owner.ts +4 -5
  232. package/src/host/loopback-ports.ts +34 -0
  233. package/src/host/replacement-module-handler.ts +24 -15
  234. package/src/host/server-scan-handler.ts +87 -0
  235. package/src/host/staged-host-files.ts +92 -0
  236. package/src/host/websocket-proxy.ts +271 -5
  237. package/src/installed-package-versions.ts +66 -0
  238. package/src/jump-to-source-babel.ts +97 -89
  239. package/src/jump-to-source-native.ts +109 -11
  240. package/src/kotlin-host.ts +144 -27
  241. package/src/kotlin-module.ts +52 -0
  242. package/src/kotlinDiscoveryContract.ts +27 -0
  243. package/src/metro-layout-parser.ts +78 -0
  244. package/src/metro-plugin.ts +129 -39
  245. package/src/metro-production-bundle.ts +140 -139
  246. package/src/native-app-identity-project.ts +103 -5
  247. package/src/native-app-identity.ts +173 -11
  248. package/src/native-firebase-config.ts +83 -80
  249. package/src/native-seam-manifest.ts +0 -44
  250. package/src/nativeAppIconContract.ts +137 -0
  251. package/src/nativeViewSourceContract.ts +49 -0
  252. package/src/nativeViewWebContract.ts +41 -0
  253. package/src/nitro-bundle.ts +43 -0
  254. package/src/nitro-compile.ts +107 -0
  255. package/src/nitro-source.ts +310 -0
  256. package/src/nitroContract.ts +51 -0
  257. package/src/one-widget-metadata.ts +23 -3
  258. package/src/oneWidgetContract.ts +36 -3
  259. package/src/packageVersionsContract.ts +52 -0
  260. package/src/process-group.ts +306 -0
  261. package/src/process-liveness.ts +24 -0
  262. package/src/process-output.ts +21 -2
  263. package/src/react-devtools-frontend-react.ts +45 -0
  264. package/src/react-native-host-modules.ts +10 -0
  265. package/src/serverDiscoveryContract.ts +41 -0
  266. package/src/skills/builtin/maestro-test.ts +2 -4
  267. package/src/swift-compile.ts +3195 -650
  268. package/src/swift-design-time.ts +552 -0
  269. package/src/swift-distribution-contract.ts +11 -0
  270. package/src/swift-distribution.lock.json +46 -11
  271. package/src/swift-host.ts +144 -30
  272. package/src/swift-icu.ts +10 -2
  273. package/src/swift-objc-sdk.ts +726 -0
  274. package/src/swift-objc-standin.ts +441 -0
  275. package/src/swift-package.ts +25 -0
  276. package/src/swift-packages.ts +25 -10
  277. package/src/swift-standin-config.ts +94 -0
  278. package/src/swift-standin-emit.ts +5 -3
  279. package/src/swift-standins.ts +225 -81
  280. package/src/swift-submit.ts +2 -1
  281. package/src/swiftContract.ts +4 -0
  282. package/src/target-matcher.ts +67 -0
  283. package/src/visit-parsed-program.ts +21 -0
  284. package/src/vite-plugin-one.ts +65 -17
  285. package/src/vite-plugin-swift.ts +1 -0
  286. package/src/vite-plugin.ts +168 -70
  287. package/src/worker-system-chunks.ts +89 -10
  288. package/src/worklet-executable.ts +21 -3
  289. package/src/worklets-babel.ts +85 -13
  290. package/src/xcode-project.ts +81 -134
  291. package/src/xcode-target-members.ts +107 -0
  292. package/cli/commands/compat.ts +0 -395
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1481 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1619 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
2
2
  let __sootsim_import_meta_url = '', __sootsim_import_meta_dirname = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; __sootsim_import_meta_dirname = __dirname; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
@@ -394,6 +394,12 @@ function resolveReplacementModuleFile(fromFile, specifier) {
394
394
  }
395
395
  return found;
396
396
  }
397
+ function isInsideAny(roots, file) {
398
+ return roots.some((root) => {
399
+ const relative2 = import_node_path.default.relative(root, file);
400
+ return !relative2.startsWith("..") && !import_node_path.default.isAbsolute(relative2);
401
+ });
402
+ }
397
403
  function isReplacementModuleRequestUrl(rawUrl) {
398
404
  if (!rawUrl) return false;
399
405
  try {
@@ -452,16 +458,11 @@ async function handleReplacementModuleRequest(req, res, options = {}) {
452
458
  return true;
453
459
  }
454
460
  const runtimePackages = new Set(url.searchParams.getAll("runtimePackage"));
455
- let resolvedProjectRoot;
456
- if (options.projectRoot) {
457
- const lexicalProjectRoot = import_node_path.default.resolve(options.projectRoot);
458
- resolvedProjectRoot = await import_promises.default.realpath(options.projectRoot);
459
- const lexicalTarget = import_node_path.default.relative(lexicalProjectRoot, targetPath);
460
- if (lexicalTarget.startsWith("..") || import_node_path.default.isAbsolute(lexicalTarget)) {
461
- res.writeHead(403, { "Content-Type": "text/plain; charset=utf-8" });
462
- res.end("replacement file must be inside the project root");
463
- return true;
464
- }
461
+ const servedRoots = options.servedRoots?.map((root) => import_node_path.default.resolve(root));
462
+ if (servedRoots && !isInsideAny(servedRoots, targetPath)) {
463
+ res.writeHead(403, { "Content-Type": "text/plain; charset=utf-8" });
464
+ res.end("replacement file must be inside a folder the bundler serves");
465
+ return true;
465
466
  }
466
467
  let resolvedTargetPath;
467
468
  try {
@@ -472,11 +473,11 @@ async function handleReplacementModuleRequest(req, res, options = {}) {
472
473
  res.end(`replacement file not found: ${message}`);
473
474
  return true;
474
475
  }
475
- if (resolvedProjectRoot) {
476
- const relativeTarget = import_node_path.default.relative(resolvedProjectRoot, resolvedTargetPath);
477
- if (relativeTarget.startsWith("..") || import_node_path.default.isAbsolute(relativeTarget)) {
476
+ if (servedRoots) {
477
+ const realRoots = (await Promise.all(servedRoots.map((root) => import_promises.default.realpath(root).catch(() => null)))).filter((root) => root !== null);
478
+ if (!isInsideAny(realRoots, resolvedTargetPath)) {
478
479
  res.writeHead(403, { "Content-Type": "text/plain; charset=utf-8" });
479
- res.end("replacement file must be inside the project root");
480
+ res.end("replacement file must be inside a folder the bundler serves");
480
481
  return true;
481
482
  }
482
483
  }
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1481 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1619 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
2
2
  let __sootsim_import_meta_url = '', __sootsim_import_meta_dirname = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; __sootsim_import_meta_dirname = __dirname; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -22,6 +22,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
22
22
  // src/host/websocket-proxy.ts
23
23
  var websocket_proxy_exports = {};
24
24
  __export(websocket_proxy_exports, {
25
+ TCP_RELAY_PATH: () => TCP_RELAY_PATH,
25
26
  WEBSOCKET_PROXY_HANDSHAKE_TIMEOUT_MS: () => WEBSOCKET_PROXY_HANDSHAKE_TIMEOUT_MS,
26
27
  WEBSOCKET_PROXY_PATH: () => WEBSOCKET_PROXY_PATH,
27
28
  handleWebSocketProxyUpgrade: () => handleWebSocketProxyUpgrade,
@@ -32,6 +33,9 @@ module.exports = __toCommonJS(websocket_proxy_exports);
32
33
  var import_crypto = require("crypto");
33
34
  var import_http = require("http");
34
35
  var import_https = require("https");
36
+ var import_net = require("net");
37
+ var import_tls = require("tls");
38
+ var import_ws = require("ws");
35
39
 
36
40
  // src/backend-origin.ts
37
41
  function normalizeHostname(hostname) {
@@ -54,8 +58,40 @@ function isTrustedBridgeRequest(headers) {
54
58
  }
55
59
  }
56
60
 
61
+ // src/is-record.ts
62
+ function isRecord(value) {
63
+ return typeof value === "object" && value !== null && !Array.isArray(value);
64
+ }
65
+
66
+ // src/host/loopback-ports.ts
67
+ var LOOPBACK_PORTS = /* @__PURE__ */ new Map();
68
+ for (const pair of process.env.PEACH_LOOPBACK_PORTS?.split(",").filter(Boolean) ?? []) {
69
+ const match = /^\s*([1-9][0-9]{0,4}):([1-9][0-9]{0,4})\s*$/.exec(pair);
70
+ const from = Number(match?.[1]);
71
+ const to = Number(match?.[2]);
72
+ if (!match || from > 65535 || to > 65535 || LOOPBACK_PORTS.has(from)) {
73
+ throw new Error(`PEACH_LOOPBACK_PORTS has an invalid entry: ${JSON.stringify(pair)}`);
74
+ }
75
+ LOOPBACK_PORTS.set(from, to);
76
+ }
77
+ function projectLoopbackPort(hostname, port) {
78
+ return isLoopbackHost(hostname) ? LOOPBACK_PORTS.get(port) ?? port : port;
79
+ }
80
+ function projectLoopbackUrl(value) {
81
+ if (LOOPBACK_PORTS.size === 0) return value;
82
+ const url = new URL(value);
83
+ const port = Number(
84
+ url.port || (url.protocol === "https:" || url.protocol === "wss:" ? 443 : 80)
85
+ );
86
+ const projected = projectLoopbackPort(url.hostname, port);
87
+ if (projected === port) return value;
88
+ url.port = String(projected);
89
+ return url.href;
90
+ }
91
+
57
92
  // src/host/websocket-proxy.ts
58
93
  var WEBSOCKET_PROXY_PATH = "/__websocket-proxy";
94
+ var TCP_RELAY_PATH = "/__tcp-relay";
59
95
  var WEBSOCKET_PROXY_HANDSHAKE_TIMEOUT_MS = 1e4;
60
96
  var WEBSOCKET_PROTOCOL_TOKEN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
61
97
  var WEBSOCKET_KEY = /^[+/0-9A-Za-z]{22}==$/;
@@ -185,20 +221,220 @@ function createUpstreamHeaders(req, targetUrl, headers, forwardOrigin) {
185
221
  }
186
222
  return upstreamHeaders;
187
223
  }
224
+ function requestPathname(rawUrl) {
225
+ if (!rawUrl) return null;
226
+ try {
227
+ return new URL(rawUrl, "http://localhost").pathname;
228
+ } catch {
229
+ return null;
230
+ }
231
+ }
188
232
  function isWebSocketProxyRequestUrl(rawUrl) {
189
- if (!rawUrl) return false;
233
+ return requestPathname(rawUrl) === WEBSOCKET_PROXY_PATH;
234
+ }
235
+ function rawDataBytes(data) {
236
+ if (Array.isArray(data)) return Buffer.concat(data);
237
+ return Buffer.isBuffer(data) ? data : Buffer.from(data);
238
+ }
239
+ function optionalString(value) {
240
+ return typeof value === "string" && value !== "" ? value : void 0;
241
+ }
242
+ function optionalNumber(value) {
243
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
244
+ }
245
+ function readTcpRelayTls(options) {
246
+ return {
247
+ rejectUnauthorized: options.rejectUnauthorized !== false,
248
+ ca: optionalString(options.ca)
249
+ };
250
+ }
251
+ function isTcpPort(value) {
252
+ return typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 65535;
253
+ }
254
+ function readTcpRelayCall(data) {
255
+ let call;
190
256
  try {
191
- return new URL(rawUrl, "http://localhost").pathname === WEBSOCKET_PROXY_PATH;
257
+ call = JSON.parse(rawDataBytes(data).toString());
192
258
  } catch {
193
- return false;
259
+ return null;
260
+ }
261
+ if (!isRecord(call)) return null;
262
+ switch (call.type) {
263
+ case "connect": {
264
+ const localAddress = optionalString(call.localAddress);
265
+ if (localAddress !== void 0 && (0, import_net.isIP)(localAddress) === 0) return null;
266
+ if (call.localPort != null && !isTcpPort(call.localPort)) return null;
267
+ return typeof call.host === "string" && !call.host.includes("\0") && isTcpPort(call.port) ? {
268
+ type: "connect",
269
+ host: call.host,
270
+ port: call.port,
271
+ localAddress,
272
+ localPort: optionalNumber(call.localPort),
273
+ connectTimeout: optionalNumber(call.connectTimeout),
274
+ tls: isRecord(call.tls) ? readTcpRelayTls(call.tls) : null
275
+ } : null;
276
+ }
277
+ case "startTLS":
278
+ return { type: "startTLS", ...readTcpRelayTls(call) };
279
+ case "end":
280
+ case "pause":
281
+ case "resume":
282
+ return { type: call.type };
283
+ case "setNoDelay":
284
+ return { type: "setNoDelay", noDelay: call.noDelay !== false };
285
+ case "setKeepAlive":
286
+ return {
287
+ type: "setKeepAlive",
288
+ enable: call.enable === true,
289
+ initialDelay: optionalNumber(call.initialDelay) ?? 0
290
+ };
291
+ default:
292
+ return null;
194
293
  }
195
294
  }
295
+ function pemCertificateBytes(pem) {
296
+ return Buffer.from(
297
+ pem.replace(/-----(BEGIN|END) CERTIFICATE-----/g, "").replace(/\s+/g, ""),
298
+ "base64"
299
+ );
300
+ }
301
+ function connectionInfo(socket) {
302
+ return {
303
+ type: "connect",
304
+ localAddress: socket.localAddress,
305
+ localPort: socket.localPort,
306
+ remoteAddress: socket.remoteAddress,
307
+ remotePort: socket.remotePort,
308
+ remoteFamily: socket.remoteFamily
309
+ };
310
+ }
311
+ var tcpRelayServer = new import_ws.WebSocketServer({ noServer: true });
312
+ function handleTcpRelay(ws) {
313
+ let socket = null;
314
+ let host = "";
315
+ let connecting = false;
316
+ let tls = false;
317
+ let held = null;
318
+ let ending = false;
319
+ const send = (event) => {
320
+ if (ws.readyState === ws.OPEN) ws.send(JSON.stringify(event));
321
+ };
322
+ const onData = (chunk) => {
323
+ if (ws.readyState === ws.OPEN) ws.send(chunk);
324
+ };
325
+ const onEnd = () => send({ type: "end" });
326
+ const onError = (error) => send({ type: "error", message: error.message });
327
+ const onClose = () => ws.close();
328
+ const relay = (next) => {
329
+ socket?.off("data", onData).off("end", onEnd).off("error", onError).off("close", onClose);
330
+ socket = next;
331
+ next.on("data", onData).on("end", onEnd).on("error", onError).on("close", onClose);
332
+ };
333
+ const write = (bytes) => {
334
+ if (ending) return;
335
+ if (held) held.push(bytes);
336
+ else socket?.write(bytes, () => send({ type: "written" }));
337
+ };
338
+ const end = (current) => current.end(() => current.destroy());
339
+ const startTls = (options, plain) => {
340
+ if (tls) return;
341
+ tls = true;
342
+ held = [];
343
+ const pinned = options.rejectUnauthorized ? options.ca : void 0;
344
+ const secured = (0, import_tls.connect)({
345
+ socket: plain,
346
+ servername: host,
347
+ rejectUnauthorized: options.rejectUnauthorized && pinned === void 0
348
+ });
349
+ relay(secured);
350
+ secured.once("secureConnect", () => {
351
+ const peer = secured.getPeerCertificate().raw;
352
+ if (pinned !== void 0 && !peer.equals(pemCertificateBytes(pinned))) {
353
+ secured.destroy(new Error("peer certificate does not match the pinned ca"));
354
+ return;
355
+ }
356
+ const queued = held ?? [];
357
+ held = null;
358
+ for (const bytes of queued) secured.write(bytes, () => send({ type: "written" }));
359
+ if (connecting) {
360
+ connecting = false;
361
+ send(connectionInfo(secured));
362
+ }
363
+ if (ending) end(secured);
364
+ });
365
+ };
366
+ ws.on("message", (data, isBinary) => {
367
+ if (isBinary) {
368
+ write(rawDataBytes(data));
369
+ return;
370
+ }
371
+ const call = readTcpRelayCall(data);
372
+ if (!call) {
373
+ onError(new Error("invalid tcp relay call"));
374
+ ws.close(1008, "invalid tcp relay call");
375
+ return;
376
+ }
377
+ if (call.type === "connect") {
378
+ if (socket) return;
379
+ host = call.host;
380
+ connecting = true;
381
+ const plain = (0, import_net.connect)({
382
+ host,
383
+ port: projectLoopbackPort(host, call.port),
384
+ localAddress: call.localAddress,
385
+ localPort: call.localPort
386
+ });
387
+ relay(plain);
388
+ const timeout = call.connectTimeout === void 0 ? void 0 : setTimeout(
389
+ () => plain.destroy(new Error("Attempt to connect to host timed out")),
390
+ Math.trunc(call.connectTimeout / 1e3) * 1e3
391
+ );
392
+ plain.once("close", () => clearTimeout(timeout));
393
+ plain.once("connect", () => {
394
+ clearTimeout(timeout);
395
+ if (!tls) send(connectionInfo(plain));
396
+ });
397
+ if (call.tls) startTls(call.tls, plain);
398
+ return;
399
+ }
400
+ const current = socket;
401
+ if (!current) return;
402
+ switch (call.type) {
403
+ case "startTLS":
404
+ startTls(call, current);
405
+ return;
406
+ case "end":
407
+ ending = true;
408
+ if (!held) end(current);
409
+ return;
410
+ case "pause":
411
+ current.pause();
412
+ return;
413
+ case "resume":
414
+ current.resume();
415
+ return;
416
+ case "setNoDelay":
417
+ current.setNoDelay(call.noDelay);
418
+ return;
419
+ case "setKeepAlive":
420
+ current.setKeepAlive(call.enable, call.initialDelay);
421
+ return;
422
+ }
423
+ });
424
+ ws.on("close", () => socket?.destroy());
425
+ ws.on("error", () => socket?.destroy());
426
+ }
196
427
  function handleWebSocketProxyUpgrade(req, socket, head, forwardOrigin = true) {
197
- if (!isWebSocketProxyRequestUrl(req.url)) return false;
428
+ const pathname = requestPathname(req.url);
429
+ if (pathname !== WEBSOCKET_PROXY_PATH && pathname !== TCP_RELAY_PATH) return false;
198
430
  if (!isSameOriginUpgrade(req)) {
199
431
  rejectUpgrade(socket, 403, "forbidden websocket proxy origin");
200
432
  return true;
201
433
  }
434
+ if (pathname === TCP_RELAY_PATH) {
435
+ tcpRelayServer.handleUpgrade(req, socket, head, handleTcpRelay);
436
+ return true;
437
+ }
202
438
  const upgradeRejection = validateUpgradeRequest(req);
203
439
  if (upgradeRejection) {
204
440
  rejectUpgrade(
@@ -231,7 +467,7 @@ function handleWebSocketProxyUpgrade(req, socket, head, forwardOrigin = true) {
231
467
  const upstreamHeaders = createUpstreamHeaders(req, targetUrl, headers, forwardOrigin);
232
468
  let settled = false;
233
469
  const request = targetUrl.protocol === "wss:" ? import_https.request : import_http.request;
234
- const upstreamRequestUrl = new URL(targetUrl);
470
+ const upstreamRequestUrl = new URL(projectLoopbackUrl(targetUrl.href));
235
471
  upstreamRequestUrl.protocol = targetUrl.protocol === "wss:" ? "https:" : "http:";
236
472
  const upstreamRequest = request(upstreamRequestUrl, {
237
473
  method: "GET",
@@ -320,6 +556,7 @@ function handleWebSocketProxyUpgrade(req, socket, head, forwardOrigin = true) {
320
556
  }
321
557
  // Annotate the CommonJS export names for ESM import in node:
322
558
  0 && (module.exports = {
559
+ TCP_RELAY_PATH,
323
560
  WEBSOCKET_PROXY_HANDSHAKE_TIMEOUT_MS,
324
561
  WEBSOCKET_PROXY_PATH,
325
562
  handleWebSocketProxyUpgrade,