peachsim 0.1.527

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 (324) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +640 -0
  3. package/cli/app-config.ts +82 -0
  4. package/cli/app-fonts.ts +388 -0
  5. package/cli/app-project.ts +59 -0
  6. package/cli/app-splash.ts +171 -0
  7. package/cli/app-state-reset.ts +26 -0
  8. package/cli/auth.ts +184 -0
  9. package/cli/bin.ts +59 -0
  10. package/cli/bridge-diagnostics.ts +226 -0
  11. package/cli/bridge-flow-runner.ts +2806 -0
  12. package/cli/browser-backend.ts +44 -0
  13. package/cli/browser-evals.ts +96 -0
  14. package/cli/cli-update.ts +261 -0
  15. package/cli/cloud-client.ts +1530 -0
  16. package/cli/cloud-dispatch.ts +206 -0
  17. package/cli/cloud-session.ts +227 -0
  18. package/cli/command-contract.ts +221 -0
  19. package/cli/command-registry.ts +304 -0
  20. package/cli/commands/agent-wrapper.ts +1048 -0
  21. package/cli/commands/agent.ts +439 -0
  22. package/cli/commands/app-fonts.ts +99 -0
  23. package/cli/commands/assert.ts +542 -0
  24. package/cli/commands/auth.ts +71 -0
  25. package/cli/commands/box/bundle-plane.ts +71 -0
  26. package/cli/commands/box/checkout-plane.ts +256 -0
  27. package/cli/commands/box/plane-share.ts +208 -0
  28. package/cli/commands/box/rnx-command.ts +60 -0
  29. package/cli/commands/box/search-command.ts +157 -0
  30. package/cli/commands/box.ts +925 -0
  31. package/cli/commands/camera.ts +266 -0
  32. package/cli/commands/cleanup.ts +169 -0
  33. package/cli/commands/compat.ts +176 -0
  34. package/cli/commands/config.ts +34 -0
  35. package/cli/commands/control.ts +2154 -0
  36. package/cli/commands/cpu-profile.ts +941 -0
  37. package/cli/commands/daemon-mac-app.ts +165 -0
  38. package/cli/commands/daemon.ts +800 -0
  39. package/cli/commands/debug.ts +751 -0
  40. package/cli/commands/desktop.ts +39 -0
  41. package/cli/commands/detect.ts +201 -0
  42. package/cli/commands/detox.ts +481 -0
  43. package/cli/commands/device.ts +134 -0
  44. package/cli/commands/diagnose.ts +589 -0
  45. package/cli/commands/do-chain.ts +408 -0
  46. package/cli/commands/electron.ts +94 -0
  47. package/cli/commands/film.ts +379 -0
  48. package/cli/commands/flow.ts +1240 -0
  49. package/cli/commands/inspect/actions.ts +516 -0
  50. package/cli/commands/inspect/core.ts +2086 -0
  51. package/cli/commands/inspect/count.ts +17 -0
  52. package/cli/commands/inspect/describe.ts +119 -0
  53. package/cli/commands/inspect/env.ts +25 -0
  54. package/cli/commands/inspect/find.ts +169 -0
  55. package/cli/commands/inspect/get-layout.ts +53 -0
  56. package/cli/commands/inspect/keyboard.ts +54 -0
  57. package/cli/commands/inspect/list.ts +175 -0
  58. package/cli/commands/inspect/memory.ts +215 -0
  59. package/cli/commands/inspect/redaction.ts +39 -0
  60. package/cli/commands/inspect/resolve-target.ts +77 -0
  61. package/cli/commands/inspect/screens.ts +78 -0
  62. package/cli/commands/inspect/settle.ts +32 -0
  63. package/cli/commands/inspect/settling.ts +197 -0
  64. package/cli/commands/inspect/shared.ts +259 -0
  65. package/cli/commands/inspect/sleep.ts +15 -0
  66. package/cli/commands/inspect/tree.ts +23 -0
  67. package/cli/commands/inspect/url.ts +17 -0
  68. package/cli/commands/inspect/wait-event.ts +211 -0
  69. package/cli/commands/inspect/wait-idle.ts +25 -0
  70. package/cli/commands/inspect/wait-ready.ts +87 -0
  71. package/cli/commands/inspect/wait-selector.ts +59 -0
  72. package/cli/commands/inspect/wait.ts +32 -0
  73. package/cli/commands/inspect.ts +4738 -0
  74. package/cli/commands/install-cli.ts +275 -0
  75. package/cli/commands/install-desktop.ts +385 -0
  76. package/cli/commands/login.ts +357 -0
  77. package/cli/commands/logout.ts +24 -0
  78. package/cli/commands/maestro-generate.ts +375 -0
  79. package/cli/commands/maestro.ts +536 -0
  80. package/cli/commands/mode.ts +58 -0
  81. package/cli/commands/no-bridge-hint.ts +80 -0
  82. package/cli/commands/perf.ts +67 -0
  83. package/cli/commands/permissions.ts +203 -0
  84. package/cli/commands/platform.ts +410 -0
  85. package/cli/commands/preview.ts +103 -0
  86. package/cli/commands/profile.ts +106 -0
  87. package/cli/commands/react.ts +353 -0
  88. package/cli/commands/record.ts +1435 -0
  89. package/cli/commands/report-issue.ts +305 -0
  90. package/cli/commands/reset.ts +89 -0
  91. package/cli/commands/runtime.ts +348 -0
  92. package/cli/commands/screenshot-command.ts +106 -0
  93. package/cli/commands/screenshot-layers.ts +144 -0
  94. package/cli/commands/screenshot-mode.ts +38 -0
  95. package/cli/commands/screenshot.ts +492 -0
  96. package/cli/commands/screenshots-capture.ts +608 -0
  97. package/cli/commands/screenshots.ts +127 -0
  98. package/cli/commands/serve.ts +139 -0
  99. package/cli/commands/setup.ts +416 -0
  100. package/cli/commands/shell-boolean-mode.ts +82 -0
  101. package/cli/commands/skills.ts +468 -0
  102. package/cli/commands/slides.ts +357 -0
  103. package/cli/commands/state.ts +94 -0
  104. package/cli/commands/storage.ts +58 -0
  105. package/cli/commands/telemetry.ts +55 -0
  106. package/cli/commands/test.ts +535 -0
  107. package/cli/commands/three-mode.ts +771 -0
  108. package/cli/commands/timeline.ts +121 -0
  109. package/cli/commands/upgrade.ts +167 -0
  110. package/cli/commands/upload.ts +1203 -0
  111. package/cli/commands/version.ts +63 -0
  112. package/cli/commands/welcome.ts +17 -0
  113. package/cli/commands/what-happened.ts +328 -0
  114. package/cli/current-sim.ts +217 -0
  115. package/cli/desktop-companion.ts +315 -0
  116. package/cli/drivers/electron.ts +70 -0
  117. package/cli/drivers/index.ts +20 -0
  118. package/cli/drivers/playwright-provisioning.ts +184 -0
  119. package/cli/drivers/playwright-sim-host.ts +537 -0
  120. package/cli/drivers/playwright.ts +209 -0
  121. package/cli/drivers/registry.ts +65 -0
  122. package/cli/drivers/types.ts +102 -0
  123. package/cli/flow-export.ts +55 -0
  124. package/cli/flow-file.ts +142 -0
  125. package/cli/flow-live-status.ts +126 -0
  126. package/cli/flow-session.ts +140 -0
  127. package/cli/help-core.ts +143 -0
  128. package/cli/help.ts +44 -0
  129. package/cli/hidden-runtime-alias.ts +19 -0
  130. package/cli/hints.ts +214 -0
  131. package/cli/inspect-notice-state.ts +114 -0
  132. package/cli/internal-child.ts +176 -0
  133. package/cli/maestro-js.ts +323 -0
  134. package/cli/main.ts +704 -0
  135. package/cli/open-url.ts +8 -0
  136. package/cli/outbound-endpoints.ts +830 -0
  137. package/cli/parent-pid.ts +204 -0
  138. package/cli/parse-args.ts +189 -0
  139. package/cli/privacy.ts +53 -0
  140. package/cli/prompt.ts +56 -0
  141. package/cli/recording-access.ts +108 -0
  142. package/cli/registry.ts +2 -0
  143. package/cli/resolve-assets.ts +63 -0
  144. package/cli/rnx-in-process.ts +98 -0
  145. package/cli/run-registry.ts +226 -0
  146. package/cli/run-rnx-core.ts +148 -0
  147. package/cli/run-rnx.ts +137 -0
  148. package/cli/runtime-notes.ts +66 -0
  149. package/cli/runtime-summary.ts +25 -0
  150. package/cli/secure-file.ts +31 -0
  151. package/cli/self-invocation.ts +58 -0
  152. package/cli/send-to-box.ts +109 -0
  153. package/cli/setup-repository.ts +191 -0
  154. package/cli/shell-init.ts +98 -0
  155. package/cli/standalone.ts +4 -0
  156. package/cli/telemetry.ts +84 -0
  157. package/cli/ui.ts +68 -0
  158. package/cli/ws-bridge.ts +1020 -0
  159. package/detox/attempt-events.cjs +23 -0
  160. package/detox/colors.ts +54 -0
  161. package/detox/config-loader.ts +135 -0
  162. package/detox/element-types.ts +48 -0
  163. package/detox/expectations.ts +534 -0
  164. package/detox/gestures.ts +501 -0
  165. package/detox/index.ts +2694 -0
  166. package/detox/jest-preset.cjs +29 -0
  167. package/detox/jest-setup-after-env.cjs +22 -0
  168. package/detox/matchers.ts +30 -0
  169. package/detox/motion-change.cjs +109 -0
  170. package/detox/motion-change.d.cts +35 -0
  171. package/detox/navigation-lifecycle.ts +290 -0
  172. package/detox/navigation.ts +43 -0
  173. package/detox/proof-frame.cjs +114 -0
  174. package/detox/record-block-execution.cjs +78 -0
  175. package/detox/screenshots/animated-color-test-rest-norngh.png +0 -0
  176. package/detox/screenshots/color-test-after-drag-norngh.png +0 -0
  177. package/detox/screenshots/color-test-rest-norngh.png +0 -0
  178. package/detox/screenshots/theme-blue-toggle.png +0 -0
  179. package/detox/screenshots/theme-blue.png +0 -0
  180. package/detox/screenshots/theme-red-toggle.png +0 -0
  181. package/detox/screenshots/theme-red.png +0 -0
  182. package/dist-lib/agent-daemon-client.cjs +502 -0
  183. package/dist-lib/agent-events.cjs +48 -0
  184. package/dist-lib/agent-identity.cjs +53 -0
  185. package/dist-lib/agent-sessions.cjs +759 -0
  186. package/dist-lib/attached-projects.cjs +526 -0
  187. package/dist-lib/auth/shared-session.cjs +208 -0
  188. package/dist-lib/backend-origin.cjs +88 -0
  189. package/dist-lib/beta.cjs +41 -0
  190. package/dist-lib/beta.mjs +13 -0
  191. package/dist-lib/bridge-constants.cjs +161 -0
  192. package/dist-lib/bridge-contract-input.cjs +451 -0
  193. package/dist-lib/bridge-contract-input.mjs +414 -0
  194. package/dist-lib/bridge-contract.cjs +130 -0
  195. package/dist-lib/bridge-contract.mjs +94 -0
  196. package/dist-lib/capture-contract.cjs +220 -0
  197. package/dist-lib/capture-contract.mjs +190 -0
  198. package/dist-lib/cli-constants.cjs +32 -0
  199. package/dist-lib/cloud-contract.cjs +194 -0
  200. package/dist-lib/cloud-contract.mjs +145 -0
  201. package/dist-lib/cloud.cjs +670 -0
  202. package/dist-lib/cloud.mjs +639 -0
  203. package/dist-lib/config.cjs +96 -0
  204. package/dist-lib/detox/index.cjs +3789 -0
  205. package/dist-lib/detox/jest-preset.cjs +29 -0
  206. package/dist-lib/dev-bundle-resolution.cjs +290 -0
  207. package/dist-lib/home-paths.cjs +638 -0
  208. package/dist-lib/host/bridge-host.cjs +10903 -0
  209. package/dist-lib/host/fetch-proxy-handler.cjs +421 -0
  210. package/dist-lib/host/fetch-proxy-overrides.cjs +63 -0
  211. package/dist-lib/host/fetch-proxy-overrides.mjs +37 -0
  212. package/dist-lib/host/replacement-module-handler.cjs +469 -0
  213. package/dist-lib/host/websocket-proxy.cjs +298 -0
  214. package/dist-lib/index.cjs +4268 -0
  215. package/dist-lib/jump-to-source-babel.cjs +93 -0
  216. package/dist-lib/jump-to-source-native.cjs +112 -0
  217. package/dist-lib/menu.cjs +646 -0
  218. package/dist-lib/menu.mjs +604 -0
  219. package/dist-lib/metro-fingerprint-registry.cjs +186 -0
  220. package/dist-lib/metro-fingerprint-registry.mjs +156 -0
  221. package/dist-lib/metro-production-bundle.cjs +329 -0
  222. package/dist-lib/metro-production-bundle.mjs +295 -0
  223. package/dist-lib/metro.cjs +904 -0
  224. package/dist-lib/profiles.cjs +246 -0
  225. package/dist-lib/public-brand.cjs +39 -0
  226. package/dist-lib/react-native-host-modules.cjs +51 -0
  227. package/dist-lib/react-native-host-modules.mjs +25 -0
  228. package/dist-lib/render-mode.cjs +54 -0
  229. package/dist-lib/scripts/dev-server-scanner.cjs +1232 -0
  230. package/dist-lib/sdk.cjs +2433 -0
  231. package/dist-lib/sdk.mjs +2336 -0
  232. package/dist-lib/skills.cjs +39132 -0
  233. package/dist-lib/swift-submit.cjs +92 -0
  234. package/dist-lib/swift-submit.mjs +65 -0
  235. package/dist-lib/swift.cjs +1646 -0
  236. package/dist-lib/vite.cjs +1591 -0
  237. package/package.json +290 -0
  238. package/scripts/dev-server-scanner.ts +1291 -0
  239. package/scripts/optional-demo-registry.ts +113 -0
  240. package/skills/contrast/SKILL.md +165 -0
  241. package/skills/rnx-debug/SKILL.md +433 -0
  242. package/skills/rnx-perf/SKILL.md +399 -0
  243. package/skills/rnx-setup/SKILL.md +172 -0
  244. package/skills/rnx-test/SKILL.md +321 -0
  245. package/skills/rnx-visual/SKILL.md +126 -0
  246. package/src/agent-daemon-client.ts +410 -0
  247. package/src/agent-events.ts +71 -0
  248. package/src/agent-identity.ts +23 -0
  249. package/src/agent-prompt.ts +71 -0
  250. package/src/agent-sessions.ts +580 -0
  251. package/src/app-url.ts +39 -0
  252. package/src/attached-projects.ts +539 -0
  253. package/src/auth/shared-session.ts +235 -0
  254. package/src/backend-origin.ts +70 -0
  255. package/src/beta.ts +19 -0
  256. package/src/bridge-constants.ts +175 -0
  257. package/src/bridge-contract-input.ts +502 -0
  258. package/src/bridge-contract.ts +558 -0
  259. package/src/browser-cache.ts +16 -0
  260. package/src/capture-contract.ts +587 -0
  261. package/src/cli-constants.ts +1 -0
  262. package/src/cli-version.ts +40 -0
  263. package/src/cloud-contract.ts +274 -0
  264. package/src/cloud.ts +762 -0
  265. package/src/codex-client.ts +234 -0
  266. package/src/config.ts +132 -0
  267. package/src/connect.ts +101 -0
  268. package/src/dev-bundle-resolution.ts +346 -0
  269. package/src/dev-server-open.ts +198 -0
  270. package/src/disk-cleanup.ts +540 -0
  271. package/src/flow-timeout-scale.ts +15 -0
  272. package/src/home-paths.ts +806 -0
  273. package/src/host/agent-host.ts +666 -0
  274. package/src/host/bridge-host.ts +2788 -0
  275. package/src/host/fetch-proxy-handler.ts +449 -0
  276. package/src/host/fetch-proxy-overrides.ts +51 -0
  277. package/src/host/open-url.ts +89 -0
  278. package/src/host/plane-host.ts +203 -0
  279. package/src/host/replacement-module-handler.ts +158 -0
  280. package/src/host/websocket-proxy.ts +315 -0
  281. package/src/index.ts +71 -0
  282. package/src/jump-to-source-babel.ts +135 -0
  283. package/src/jump-to-source-native.ts +102 -0
  284. package/src/menu.ts +777 -0
  285. package/src/metro-fingerprint-registry.ts +302 -0
  286. package/src/metro-plugin.ts +341 -0
  287. package/src/metro-production-bundle.ts +471 -0
  288. package/src/native-dev-bundle-url.ts +118 -0
  289. package/src/native-seam-manifest.ts +341 -0
  290. package/src/playwright-host-log.ts +16 -0
  291. package/src/plist.ts +27 -0
  292. package/src/poll-until.ts +123 -0
  293. package/src/profiles.ts +179 -0
  294. package/src/public-brand.ts +9 -0
  295. package/src/react-native-host-modules.ts +26 -0
  296. package/src/render-mode.ts +27 -0
  297. package/src/rolldown-plugin-swift.ts +240 -0
  298. package/src/runtime-assets.ts +233 -0
  299. package/src/runtime-delivery.ts +54 -0
  300. package/src/screenshots/compose.ts +422 -0
  301. package/src/screenshots/frame-compose.ts +514 -0
  302. package/src/screenshots/orchestrate.ts +256 -0
  303. package/src/screenshots/org-deck.ts +91 -0
  304. package/src/screenshots/registry.ts +58 -0
  305. package/src/screenshots/schema.ts +364 -0
  306. package/src/sdk.ts +15 -0
  307. package/src/settings-bundle.ts +418 -0
  308. package/src/shell-bridge-identity.ts +15 -0
  309. package/src/sim-client.ts +697 -0
  310. package/src/skills/builtin/a11y-review.ts +132 -0
  311. package/src/skills/builtin/compat-check.ts +71 -0
  312. package/src/skills/builtin/maestro-test.ts +121 -0
  313. package/src/skills/builtin/perf-profile.ts +90 -0
  314. package/src/skills/builtin/screenshot-all.ts +52 -0
  315. package/src/skills/builtin/visual-diff.ts +100 -0
  316. package/src/skills/registry.ts +107 -0
  317. package/src/skills/types.ts +41 -0
  318. package/src/swift-submit.ts +185 -0
  319. package/src/swift.ts +7 -0
  320. package/src/team-machine-prompt.ts +73 -0
  321. package/src/vite-plugin-one.ts +264 -0
  322. package/src/vite-plugin-swift.ts +620 -0
  323. package/src/vite-plugin.ts +1946 -0
  324. package/src/worklets-babel.ts +250 -0
@@ -0,0 +1,449 @@
1
+ // shared fetch-proxy + app-api handlers for the sootsim dev/daemon HTTP
2
+ // surface. both the vite dev middleware (packages/sootsim-shell/src/
3
+ // dev-middleware.ts) and the daemon bridge (packages/sootsim/src/host/
4
+ // bridge-host.ts) mount these on their respective servers so guest bundle
5
+ // fetches resolve the same way regardless of which surface the sim is
6
+ // loaded through.
7
+ //
8
+ // the symptom that drove this extraction: when the shell dev server was
9
+ // not running, sims fell back to the daemon's loopback HTTP server,
10
+ // which only knew about `/__bundle-proxy` and let every `/__fetch-proxy`
11
+ // or `/__app-api` request fall through to the SPA index.html. tenant
12
+ // code (e.g. Expensify's NetInfo reachability poll) saw a 200 response
13
+ // whose body was the shell HTML, parsed it as JSON, failed, and reported
14
+ // `isInternetReachable: false` → "you appear to be offline".
15
+ //
16
+ // routes implemented here:
17
+ // /__fetch-proxy?url=… — generic CORS-bypassing proxy
18
+ // /__proxy?url=… — alias of /__fetch-proxy (demo-gateway path)
19
+ // /__app-api?origin=…&path=… — tenant API reverse proxy, stateless
20
+ // (bundle origin carried in the query string)
21
+ // /__app-api/<path> — legacy form for callers that registered an
22
+ // origin separately. requires an out-of-band
23
+ // tenant-origin registry to resolve; left as
24
+ // the caller's responsibility.
25
+
26
+ import http, { type IncomingMessage, type ServerResponse } from 'http'
27
+ import https from 'https'
28
+ import { finished, pipeline } from 'stream/promises'
29
+ import { isLoopbackHost, normalizeHostname } from '../backend-origin.ts'
30
+ import {
31
+ FETCH_PROXY_BROWSER_USER_AGENT,
32
+ getFetchProxyTargetHeaders,
33
+ } from './fetch-proxy-overrides.ts'
34
+
35
+ const STRIP_FETCH_PROXY_HEADERS = new Set([
36
+ 'host',
37
+ 'origin',
38
+ 'referer',
39
+ 'user-agent',
40
+ 'accept-encoding',
41
+ 'cookie',
42
+ 'connection',
43
+ 'keep-alive',
44
+ 'transfer-encoding',
45
+ 'upgrade',
46
+ 'content-length',
47
+ 'sec-fetch-site',
48
+ 'sec-fetch-mode',
49
+ 'sec-fetch-dest',
50
+ 'sec-ch-ua',
51
+ 'sec-ch-ua-mobile',
52
+ 'sec-ch-ua-platform',
53
+ ])
54
+
55
+ const FETCH_PROXY_CORS_HEADERS: Record<string, string> = {
56
+ 'access-control-allow-origin': '*',
57
+ 'access-control-allow-methods': 'GET,POST,PUT,DELETE,PATCH,OPTIONS',
58
+ 'access-control-allow-headers': '*',
59
+ 'access-control-expose-headers': '*',
60
+ 'access-control-max-age': '3600',
61
+ }
62
+
63
+ const APP_API_HEADER_REWRITES = new Set([
64
+ 'host',
65
+ 'origin',
66
+ 'referer',
67
+ 'sec-fetch-site',
68
+ 'sec-fetch-mode',
69
+ 'sec-fetch-dest',
70
+ ])
71
+
72
+ // a developer's local https stack signs with a locally-trusted CA (mkcert et
73
+ // al) that node's bundled root store cannot see, so proxied requests to it die
74
+ // with SELF_SIGNED_CERT_IN_CHAIN. traffic to a loopback target never leaves
75
+ // the machine, so certificate validation adds nothing there — accept it rather
76
+ // than requiring every host to plumb a CA bundle into a long-lived daemon.
77
+ function loopbackTlsOptions(targetUrl: URL): { rejectUnauthorized?: boolean } {
78
+ if (targetUrl.protocol !== 'https:') return {}
79
+ return isLoopbackHost(targetUrl.hostname) ? { rejectUnauthorized: false } : {}
80
+ }
81
+
82
+ function applyFetchProxyCors(res: ServerResponse) {
83
+ for (const [key, value] of Object.entries(FETCH_PROXY_CORS_HEADERS)) {
84
+ res.setHeader(key, value)
85
+ }
86
+ }
87
+
88
+ function formatFetchProxyError(targetUrl: string, err: unknown): string {
89
+ const details: string[] = []
90
+ const error = err as
91
+ | {
92
+ message?: string
93
+ code?: string
94
+ cause?: { message?: string; code?: string }
95
+ }
96
+ | undefined
97
+
98
+ if (error?.code) details.push(error.code)
99
+ if (error?.message) details.push(error.message)
100
+ if (error?.cause?.code) details.push(error.cause.code)
101
+ if (error?.cause?.message) details.push(error.cause.message)
102
+
103
+ const uniqueDetails = [...new Set(details.filter(Boolean))]
104
+ const message = uniqueDetails.join(' | ') || String(err)
105
+
106
+ if (targetUrl.includes('stored-in-.env.local')) {
107
+ return `${message} | upstream url still contains placeholder env values`
108
+ }
109
+
110
+ return message
111
+ }
112
+
113
+ export function buildFetchProxyHeaders(
114
+ reqHeaders: Record<string, string | string[] | undefined>,
115
+ targetUrl?: URL,
116
+ ): Record<string, string> {
117
+ const headers: Record<string, string> = {}
118
+ for (const [key, value] of Object.entries(reqHeaders)) {
119
+ if (!value) continue
120
+ if (STRIP_FETCH_PROXY_HEADERS.has(key.toLowerCase())) continue
121
+ headers[key] = Array.isArray(value) ? value.join(', ') : value
122
+ }
123
+ Object.assign(
124
+ headers,
125
+ targetUrl
126
+ ? getFetchProxyTargetHeaders(targetUrl)
127
+ : { 'user-agent': FETCH_PROXY_BROWSER_USER_AGENT },
128
+ )
129
+ return headers
130
+ }
131
+
132
+ export function buildAppApiProxyHeaders(
133
+ reqHeaders: Record<string, string | string[] | undefined>,
134
+ targetUrl: URL,
135
+ ): Record<string, string | string[]> {
136
+ const headers: Record<string, string | string[]> = {}
137
+ for (const [key, value] of Object.entries(reqHeaders)) {
138
+ if (!value) continue
139
+ if (APP_API_HEADER_REWRITES.has(key.toLowerCase())) continue
140
+ headers[key] = value
141
+ }
142
+ headers.host = targetUrl.host
143
+ if (!Object.keys(headers).some((name) => name.toLowerCase() === 'expo-origin')) {
144
+ headers.origin = targetUrl.origin
145
+ headers.referer = `${targetUrl.origin}/`
146
+ }
147
+ return headers
148
+ }
149
+
150
+ export function isFetchProxyRequestUrl(rawUrl: string | undefined): boolean {
151
+ return rawUrl?.startsWith('/__fetch-proxy?') || rawUrl?.startsWith('/__proxy?') || false
152
+ }
153
+
154
+ export function isBundleProxyRequestUrl(rawUrl: string | undefined): boolean {
155
+ return rawUrl?.startsWith('/__bundle-proxy?') ?? false
156
+ }
157
+
158
+ export function isAppApiRequestUrl(rawUrl: string | undefined): boolean {
159
+ if (!rawUrl) return false
160
+ if (rawUrl.startsWith('/__app-api?')) return true
161
+ if (rawUrl.startsWith('/__app-api/')) return true
162
+ return false
163
+ }
164
+
165
+ export async function handleFetchProxyRequest(
166
+ req: IncomingMessage,
167
+ res: ServerResponse,
168
+ ): Promise<void> {
169
+ if (req.method === 'OPTIONS') {
170
+ applyFetchProxyCors(res)
171
+ res.writeHead(204)
172
+ res.end()
173
+ return
174
+ }
175
+
176
+ const params = new URLSearchParams((req.url || '').split('?')[1] || '')
177
+ const targetUrl = params.get('url')
178
+ if (!targetUrl) {
179
+ applyFetchProxyCors(res)
180
+ res.writeHead(400, { 'Content-Type': 'text/plain' })
181
+ res.end('missing url param')
182
+ return
183
+ }
184
+
185
+ let upstreamUrl: URL
186
+ try {
187
+ upstreamUrl = new URL(targetUrl)
188
+ } catch {
189
+ applyFetchProxyCors(res)
190
+ res.writeHead(400, { 'Content-Type': 'text/plain' })
191
+ res.end('invalid url param')
192
+ return
193
+ }
194
+
195
+ let body: Buffer | undefined
196
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
197
+ const chunks: Buffer[] = []
198
+ for await (const chunk of req) {
199
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))
200
+ }
201
+ if (chunks.length > 0) {
202
+ body = Buffer.concat(chunks)
203
+ }
204
+ }
205
+
206
+ const originalMethod = req.method || 'GET'
207
+ const proxy = async (
208
+ targetUrl: URL,
209
+ method: string,
210
+ nextBody: Buffer | undefined,
211
+ redirects: number,
212
+ ): Promise<void> => {
213
+ const transport = targetUrl.protocol === 'https:' ? https : http
214
+ const targetHeaders = buildFetchProxyHeaders(req.headers, targetUrl)
215
+ if (targetUrl.origin !== upstreamUrl.origin) {
216
+ delete targetHeaders.authorization
217
+ delete targetHeaders['proxy-authorization']
218
+ }
219
+ if (method === 'GET' || method === 'HEAD') {
220
+ delete targetHeaders['content-length']
221
+ delete targetHeaders['content-type']
222
+ } else if (nextBody) {
223
+ targetHeaders['content-length'] = String(nextBody.byteLength)
224
+ }
225
+
226
+ const proxyRes = await new Promise<IncomingMessage>((resolve, reject) => {
227
+ const proxyReq = transport.request(
228
+ {
229
+ hostname: normalizeHostname(targetUrl.hostname),
230
+ port: targetUrl.port || (targetUrl.protocol === 'https:' ? 443 : 80),
231
+ path: targetUrl.pathname + targetUrl.search,
232
+ method,
233
+ headers: targetHeaders,
234
+ ...loopbackTlsOptions(targetUrl),
235
+ },
236
+ resolve,
237
+ )
238
+ const abort = () => proxyReq.destroy(new Error('fetch proxy client disconnected'))
239
+ req.once('aborted', abort)
240
+ proxyReq.once('close', () => req.off('aborted', abort))
241
+ proxyReq.once('error', reject)
242
+ if (nextBody === undefined) proxyReq.end()
243
+ else proxyReq.end(nextBody)
244
+ })
245
+
246
+ const status = proxyRes.statusCode ?? 502
247
+ const location = proxyRes.headers.location
248
+ if (
249
+ location &&
250
+ (status === 301 ||
251
+ status === 302 ||
252
+ status === 303 ||
253
+ status === 307 ||
254
+ status === 308)
255
+ ) {
256
+ if (redirects >= 10) {
257
+ proxyRes.destroy()
258
+ throw new Error('too many redirects')
259
+ }
260
+ const switchToGet =
261
+ ((status === 301 || status === 302) && method === 'POST') ||
262
+ (status === 303 && method !== 'GET' && method !== 'HEAD')
263
+ const redirectUrl = new URL(location, targetUrl)
264
+ proxyRes.resume()
265
+ await finished(proxyRes)
266
+ await proxy(
267
+ redirectUrl,
268
+ switchToGet ? 'GET' : method,
269
+ switchToGet ? undefined : nextBody,
270
+ redirects + 1,
271
+ )
272
+ return
273
+ }
274
+
275
+ for (const [key, value] of Object.entries(proxyRes.headers)) {
276
+ const lowerKey = key.toLowerCase()
277
+ if (
278
+ value === undefined ||
279
+ lowerKey === 'set-cookie' ||
280
+ lowerKey === 'connection' ||
281
+ lowerKey === 'keep-alive' ||
282
+ lowerKey === 'proxy-authenticate' ||
283
+ lowerKey === 'proxy-authorization' ||
284
+ lowerKey === 'te' ||
285
+ lowerKey === 'trailer' ||
286
+ lowerKey === 'transfer-encoding' ||
287
+ lowerKey === 'upgrade' ||
288
+ lowerKey.startsWith('access-control-')
289
+ ) {
290
+ continue
291
+ }
292
+ res.setHeader(key, value)
293
+ }
294
+ applyFetchProxyCors(res)
295
+ const setCookie = proxyRes.headers['set-cookie'] ?? []
296
+ if (setCookie.length > 0) {
297
+ res.setHeader(
298
+ 'x-sootsim-set-cookie',
299
+ (Array.isArray(setCookie) ? setCookie : [setCookie]).join(', '),
300
+ )
301
+ }
302
+ res.statusCode = status
303
+ await pipeline(proxyRes, res)
304
+ }
305
+
306
+ try {
307
+ await proxy(upstreamUrl, originalMethod, body, 0)
308
+ } catch (err) {
309
+ if (res.headersSent) {
310
+ res.destroy(err instanceof Error ? err : new Error(String(err)))
311
+ return
312
+ }
313
+ const norm = normalizeHostname(upstreamUrl.hostname)
314
+ if (isLoopbackHost(norm) && (err as { code?: string })?.code === 'ECONNREFUSED') {
315
+ const altHost =
316
+ norm === '::1' ? '127.0.0.1' : norm === '127.0.0.1' ? '::1' : '127.0.0.1'
317
+ if (altHost !== norm) {
318
+ try {
319
+ const altUrl = new URL(upstreamUrl.href)
320
+ altUrl.hostname = altHost
321
+ await proxy(altUrl, originalMethod, body, 0)
322
+ return
323
+ } catch {}
324
+ }
325
+ }
326
+ applyFetchProxyCors(res)
327
+ res.writeHead(502, { 'Content-Type': 'text/plain' })
328
+ res.end(`fetch proxy error: ${formatFetchProxyError(upstreamUrl.href, err)}`)
329
+ }
330
+ }
331
+
332
+ export async function handleBundleProxyRequest(
333
+ req: IncomingMessage,
334
+ res: ServerResponse,
335
+ ): Promise<void> {
336
+ const params = new URLSearchParams((req.url || '').split('?')[1] || '')
337
+ const target = params.get('url')
338
+ if (!target) {
339
+ applyFetchProxyCors(res)
340
+ res.writeHead(400, { 'Content-Type': 'text/plain' })
341
+ res.end('bundle-proxy: missing url query param')
342
+ return
343
+ }
344
+
345
+ let targetUrl: URL
346
+ try {
347
+ targetUrl = new URL(target)
348
+ } catch {
349
+ applyFetchProxyCors(res)
350
+ res.writeHead(400, { 'Content-Type': 'text/plain' })
351
+ res.end('bundle-proxy: invalid url')
352
+ return
353
+ }
354
+
355
+ if (!isLoopbackHost(targetUrl.hostname)) {
356
+ applyFetchProxyCors(res)
357
+ res.writeHead(403, { 'Content-Type': 'text/plain' })
358
+ res.end('bundle-proxy: only loopback targets allowed')
359
+ return
360
+ }
361
+
362
+ await handleFetchProxyRequest(req, res)
363
+ }
364
+
365
+ export function handleAppApiRequest(req: IncomingMessage, res: ServerResponse): boolean {
366
+ // returns true if it handled the request, false if the caller should fall
367
+ // through. /__app-api requires `origin` query param for stateless mode;
368
+ // without it we return false so the caller decides what to do (the vite
369
+ // middleware uses its own out-of-band origin registry; the daemon has
370
+ // no such registry so it returns 400).
371
+
372
+ const reqUrl = req.url || ''
373
+
374
+ let targetPath = ''
375
+ let targetOrigin = ''
376
+ if (reqUrl.startsWith('/__app-api?')) {
377
+ const parsed = new URL(reqUrl, 'http://sootsim.local')
378
+ targetPath = parsed.searchParams.get('path') || ''
379
+ targetOrigin = parsed.searchParams.get('origin')?.trim() || ''
380
+ } else if (reqUrl.startsWith('/__app-api/')) {
381
+ targetPath = reqUrl.slice('/__app-api'.length)
382
+ } else {
383
+ return false
384
+ }
385
+
386
+ if (!targetOrigin) {
387
+ res.writeHead(400, { 'Content-Type': 'text/plain' })
388
+ res.end('app-api: missing origin query param')
389
+ return true
390
+ }
391
+
392
+ if (req.method === 'OPTIONS') {
393
+ res.writeHead(204, {
394
+ 'Access-Control-Allow-Origin': (req.headers.origin as string) || '*',
395
+ 'Access-Control-Allow-Methods': 'GET,POST,PUT,PATCH,DELETE,OPTIONS',
396
+ 'Access-Control-Allow-Headers':
397
+ (req.headers['access-control-request-headers'] as string) || '*',
398
+ 'Access-Control-Allow-Credentials': 'true',
399
+ 'Access-Control-Max-Age': '86400',
400
+ })
401
+ res.end()
402
+ return true
403
+ }
404
+
405
+ let targetUrl: URL
406
+ try {
407
+ targetUrl = new URL(targetPath, targetOrigin)
408
+ } catch {
409
+ res.writeHead(400, { 'Content-Type': 'text/plain' })
410
+ res.end('app-api: invalid origin or path')
411
+ return true
412
+ }
413
+
414
+ const transport = targetUrl.protocol === 'https:' ? https : http
415
+
416
+ const fwdHeaders = buildAppApiProxyHeaders(req.headers, targetUrl)
417
+
418
+ const proxyReq = transport.request(
419
+ {
420
+ hostname: normalizeHostname(targetUrl.hostname),
421
+ port: targetUrl.port || (targetUrl.protocol === 'https:' ? 443 : 80),
422
+ path: targetUrl.pathname + targetUrl.search,
423
+ method: req.method,
424
+ headers: fwdHeaders,
425
+ ...loopbackTlsOptions(targetUrl),
426
+ },
427
+ (proxyRes) => {
428
+ const exposedHeaders = Object.keys(proxyRes.headers)
429
+ .filter((name) => {
430
+ const lower = name.toLowerCase()
431
+ return !lower.startsWith('access-control-') && lower !== 'set-cookie'
432
+ })
433
+ .join(', ')
434
+ res.writeHead(proxyRes.statusCode ?? 502, {
435
+ ...proxyRes.headers,
436
+ 'access-control-allow-origin': (req.headers.origin as string) || '*',
437
+ 'access-control-allow-credentials': 'true',
438
+ 'access-control-expose-headers': exposedHeaders,
439
+ })
440
+ proxyRes.pipe(res)
441
+ },
442
+ )
443
+ proxyReq.on('error', (err) => {
444
+ res.statusCode = 502
445
+ res.end(`app proxy error: ${err.message}`)
446
+ })
447
+ req.pipe(proxyReq)
448
+ return true
449
+ }
@@ -0,0 +1,51 @@
1
+ // shared upstream header policy for SootSim's CORS-bypassing fetch proxy.
2
+ // requests still forward tenant auth and app headers, but the proxy transport
3
+ // defaults to native-app semantics: no browser Origin or Referer. web-backed
4
+ // mobile APIs that require a specific web origin opt in below.
5
+
6
+ export const FETCH_PROXY_BROWSER_USER_AGENT =
7
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 ' +
8
+ '(KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36'
9
+
10
+ const HOST_HEADER_OVERRIDES: Array<{
11
+ hostSuffix: string
12
+ headers: Record<string, string>
13
+ suppressBrowserUserAgent?: boolean
14
+ }> = [
15
+ {
16
+ hostSuffix: 'uniswap.org',
17
+ headers: {
18
+ origin: 'https://app.uniswap.org',
19
+ referer: 'https://app.uniswap.org/',
20
+ },
21
+ // uniswap's native gateway accepts the app origin, but forcing a desktop
22
+ // browser ua on native api posts returns a bogus 414/tcp-stack response.
23
+ suppressBrowserUserAgent: true,
24
+ },
25
+ ]
26
+
27
+ function headerOverridesFor(hostname: string):
28
+ | {
29
+ headers: Record<string, string>
30
+ suppressBrowserUserAgent?: boolean
31
+ }
32
+ | undefined {
33
+ const host = hostname.toLowerCase()
34
+ for (const override of HOST_HEADER_OVERRIDES) {
35
+ if (host === override.hostSuffix || host.endsWith(`.${override.hostSuffix}`)) {
36
+ return override
37
+ }
38
+ }
39
+ return undefined
40
+ }
41
+
42
+ export function getFetchProxyTargetHeaders(targetUrl: URL): Record<string, string> {
43
+ const hostOverrides = headerOverridesFor(targetUrl.hostname)
44
+ return {
45
+ 'accept-encoding': 'identity',
46
+ ...(hostOverrides?.suppressBrowserUserAgent
47
+ ? {}
48
+ : { 'user-agent': FETCH_PROXY_BROWSER_USER_AGENT }),
49
+ ...hostOverrides?.headers,
50
+ }
51
+ }
@@ -0,0 +1,89 @@
1
+ import { spawn } from 'child_process'
2
+
3
+ export type OpenUrlLaunchVia = 'system'
4
+
5
+ export interface OpenUrlOptions {
6
+ detached?: boolean
7
+ background?: boolean
8
+ }
9
+
10
+ export interface OpenUrlCommandOptions extends OpenUrlOptions {
11
+ platform?: NodeJS.Platform
12
+ }
13
+
14
+ export interface OpenUrlCommand {
15
+ command: string
16
+ args: string[]
17
+ via: OpenUrlLaunchVia
18
+ target?: string
19
+ }
20
+
21
+ export interface OpenUrlLaunchResult extends OpenUrlCommand {
22
+ pid?: number
23
+ attachUrl: string
24
+ }
25
+
26
+ export function buildOpenUrlCommand(
27
+ url: string,
28
+ options: OpenUrlCommandOptions = {},
29
+ ): OpenUrlCommand {
30
+ if (!url) throw new Error('openUrl requires a url')
31
+
32
+ const platform = options.platform ?? process.platform
33
+ if (platform === 'darwin') {
34
+ return {
35
+ command: 'open',
36
+ args: options.background === false ? [url] : ['-g', url],
37
+ via: 'system',
38
+ }
39
+ }
40
+
41
+ if (platform === 'win32') {
42
+ return {
43
+ command: 'cmd',
44
+ args: ['/c', 'start', '', url],
45
+ via: 'system',
46
+ }
47
+ }
48
+
49
+ return {
50
+ command: 'xdg-open',
51
+ args: [url],
52
+ via: 'system',
53
+ }
54
+ }
55
+
56
+ async function spawnLaunch(
57
+ command: string,
58
+ args: string[],
59
+ detached: boolean,
60
+ ): Promise<number | undefined> {
61
+ return new Promise((resolve, reject) => {
62
+ const child = spawn(command, args, {
63
+ detached,
64
+ stdio: 'ignore',
65
+ })
66
+ child.once('error', reject)
67
+ child.once('spawn', () => {
68
+ if (detached) child.unref()
69
+ resolve(child.pid)
70
+ })
71
+ })
72
+ }
73
+
74
+ export async function launchUrl(
75
+ url: string,
76
+ options: OpenUrlOptions = {},
77
+ ): Promise<OpenUrlLaunchResult> {
78
+ const command = buildOpenUrlCommand(url, options)
79
+ const pid = await spawnLaunch(command.command, command.args, options.detached ?? true)
80
+ return {
81
+ ...command,
82
+ pid,
83
+ attachUrl: url,
84
+ }
85
+ }
86
+
87
+ export async function openUrl(url: string, options: OpenUrlOptions = {}): Promise<void> {
88
+ await launchUrl(url, options)
89
+ }