rnxsim 0.0.0 → 0.1.313

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 (228) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +512 -0
  3. package/detox/colors.ts +54 -0
  4. package/detox/config-loader.ts +135 -0
  5. package/detox/element-types.ts +39 -0
  6. package/detox/expectations.ts +537 -0
  7. package/detox/gestures.ts +480 -0
  8. package/detox/index.ts +2118 -0
  9. package/detox/jest-preset.cjs +51 -0
  10. package/detox/jest-setup-after-env.cjs +18 -0
  11. package/detox/matchers.ts +30 -0
  12. package/detox/motion-change.cjs +109 -0
  13. package/detox/motion-change.d.cts +35 -0
  14. package/detox/navigation.ts +43 -0
  15. package/detox/screenshots/animated-color-test-rest-norngh.png +0 -0
  16. package/detox/screenshots/color-test-after-drag-norngh.png +0 -0
  17. package/detox/screenshots/color-test-rest-norngh.png +0 -0
  18. package/detox/screenshots/theme-blue-toggle.png +0 -0
  19. package/detox/screenshots/theme-blue.png +0 -0
  20. package/detox/screenshots/theme-red-toggle.png +0 -0
  21. package/detox/screenshots/theme-red.png +0 -0
  22. package/dist-cli/bin.js +15 -0
  23. package/dist-cli/chunks/agent-XZ2KTPCU.js +48 -0
  24. package/dist-cli/chunks/agent-wrapper-JJYYW2WH.js +17 -0
  25. package/dist-cli/chunks/app-fonts-IXRNQG6B.js +4 -0
  26. package/dist-cli/chunks/assert-54T5SK5F.js +49 -0
  27. package/dist-cli/chunks/auth-FI5UDI45.js +12 -0
  28. package/dist-cli/chunks/beta-JV6UKADW.js +4 -0
  29. package/dist-cli/chunks/camera-VL73YIKP.js +22 -0
  30. package/dist-cli/chunks/chunk-2D2UPBBR.js +4 -0
  31. package/dist-cli/chunks/chunk-2YR5BGA5.js +5 -0
  32. package/dist-cli/chunks/chunk-3NV2NCNX.js +4 -0
  33. package/dist-cli/chunks/chunk-46EUUFJ5.js +4 -0
  34. package/dist-cli/chunks/chunk-4NPPOV2N.js +5 -0
  35. package/dist-cli/chunks/chunk-5DHC6KHQ.js +7 -0
  36. package/dist-cli/chunks/chunk-5TEF3ET3.js +74 -0
  37. package/dist-cli/chunks/chunk-5TPRP5QT.js +4 -0
  38. package/dist-cli/chunks/chunk-5YJCOWCH.js +4 -0
  39. package/dist-cli/chunks/chunk-7GN3LVWB.js +4 -0
  40. package/dist-cli/chunks/chunk-BBULZ7CG.js +568 -0
  41. package/dist-cli/chunks/chunk-BTWORNNG.js +71 -0
  42. package/dist-cli/chunks/chunk-D4FFVGI5.js +5 -0
  43. package/dist-cli/chunks/chunk-DCEMHR2Y.js +4 -0
  44. package/dist-cli/chunks/chunk-DZS6WPUI.js +4 -0
  45. package/dist-cli/chunks/chunk-F5ZRSS3C.js +3 -0
  46. package/dist-cli/chunks/chunk-FSUYIVJ6.js +9 -0
  47. package/dist-cli/chunks/chunk-G2WW6L2C.js +23 -0
  48. package/dist-cli/chunks/chunk-GASE6UBA.js +4 -0
  49. package/dist-cli/chunks/chunk-GGRX24GF.js +142 -0
  50. package/dist-cli/chunks/chunk-HI5TFJWN.js +5 -0
  51. package/dist-cli/chunks/chunk-IJ5CAZZC.js +5 -0
  52. package/dist-cli/chunks/chunk-IJO63TDP.js +4 -0
  53. package/dist-cli/chunks/chunk-KTHV3RUS.js +26 -0
  54. package/dist-cli/chunks/chunk-LF2ZVT7O.js +6 -0
  55. package/dist-cli/chunks/chunk-MJRLLB4R.js +38 -0
  56. package/dist-cli/chunks/chunk-NFK7T35W.js +4 -0
  57. package/dist-cli/chunks/chunk-NMF2ZMZQ.js +15 -0
  58. package/dist-cli/chunks/chunk-OHAZNXLK.js +4 -0
  59. package/dist-cli/chunks/chunk-OVFJFXUD.js +101 -0
  60. package/dist-cli/chunks/chunk-OZSSI4WN.js +28 -0
  61. package/dist-cli/chunks/chunk-QKDWYITG.js +25 -0
  62. package/dist-cli/chunks/chunk-RSZWCKNT.js +30 -0
  63. package/dist-cli/chunks/chunk-RTN5C5RL.js +4 -0
  64. package/dist-cli/chunks/chunk-TIVZIMMW.js +4 -0
  65. package/dist-cli/chunks/chunk-TZFFR3SD.js +7 -0
  66. package/dist-cli/chunks/chunk-UC6U3MML.js +5 -0
  67. package/dist-cli/chunks/chunk-VFCMSYZK.js +6 -0
  68. package/dist-cli/chunks/chunk-VNQEB4L7.js +5 -0
  69. package/dist-cli/chunks/chunk-VZXWHRUZ.js +983 -0
  70. package/dist-cli/chunks/chunk-W6K4EFPH.js +107 -0
  71. package/dist-cli/chunks/chunk-WEXDAC74.js +4 -0
  72. package/dist-cli/chunks/chunk-WF3T4SVI.js +14 -0
  73. package/dist-cli/chunks/chunk-WINYQ44O.js +4 -0
  74. package/dist-cli/chunks/chunk-WMIIKMGK.js +10 -0
  75. package/dist-cli/chunks/chunk-WUSWBCWA.js +507 -0
  76. package/dist-cli/chunks/chunk-WWZIXIRD.js +4 -0
  77. package/dist-cli/chunks/chunk-XEVZYVIW.js +766 -0
  78. package/dist-cli/chunks/chunk-YDGQTMQL.js +4 -0
  79. package/dist-cli/chunks/chunk-YIFT42WN.js +4 -0
  80. package/dist-cli/chunks/chunk-ZMJD5GEC.js +125 -0
  81. package/dist-cli/chunks/cleanup-P27PA6JI.js +25 -0
  82. package/dist-cli/chunks/cli-version-WWLPBDQ7.js +4 -0
  83. package/dist-cli/chunks/compat-ZD65FED3.js +20 -0
  84. package/dist-cli/chunks/config-XMJRNM2A.js +13 -0
  85. package/dist-cli/chunks/control-KMIQT3QP.js +4 -0
  86. package/dist-cli/chunks/daemon-G2ME7NLB.js +4 -0
  87. package/dist-cli/chunks/debug-PT4HOP7N.js +200 -0
  88. package/dist-cli/chunks/desktop-S3FG72AK.js +31 -0
  89. package/dist-cli/chunks/detox-B3D4IFCN.js +52 -0
  90. package/dist-cli/chunks/device-XBNDSB2R.js +18 -0
  91. package/dist-cli/chunks/diagnose-HMQXJE5N.js +43 -0
  92. package/dist-cli/chunks/disk-cleanup-BLCZ5BSZ.js +4 -0
  93. package/dist-cli/chunks/drivers-LDECZGP2.js +4 -0
  94. package/dist-cli/chunks/film-BJGTBYZB.js +74 -0
  95. package/dist-cli/chunks/flow-UEQNVTU7.js +4 -0
  96. package/dist-cli/chunks/help-T5FYSVGB.js +4 -0
  97. package/dist-cli/chunks/hidden-runtime-alias-ANOYADHM.js +4 -0
  98. package/dist-cli/chunks/home-paths-GT3LFNOR.js +4 -0
  99. package/dist-cli/chunks/inspect-ZA6XF5LD.js +4 -0
  100. package/dist-cli/chunks/install-desktop-TIMUDHPL.js +4 -0
  101. package/dist-cli/chunks/login-FJ737MWG.js +28 -0
  102. package/dist-cli/chunks/logout-ZCNMMHMY.js +4 -0
  103. package/dist-cli/chunks/maestro-SZTNKLDF.js +104 -0
  104. package/dist-cli/chunks/maestro-generate-DCFAIZ4H.js +36 -0
  105. package/dist-cli/chunks/mode-GRMQCRXR.js +26 -0
  106. package/dist-cli/chunks/optional-demo-registry-W36EWFFB.js +4 -0
  107. package/dist-cli/chunks/perf-QYBAAUZG.js +48 -0
  108. package/dist-cli/chunks/permissions-3QCQ6VF4.js +13 -0
  109. package/dist-cli/chunks/record-QPPC2S4E.js +72 -0
  110. package/dist-cli/chunks/report-issue-7NMFP4HK.js +29 -0
  111. package/dist-cli/chunks/runtime-XOAXMSTU.js +4 -0
  112. package/dist-cli/chunks/screenshot-command-67AECJFB.js +159 -0
  113. package/dist-cli/chunks/screenshot-layers-ASWBYPJL.js +20 -0
  114. package/dist-cli/chunks/screenshots-capture-PXA3HFQK.js +42 -0
  115. package/dist-cli/chunks/serve-BI2NBAXG.js +44 -0
  116. package/dist-cli/chunks/setup-7DWPMRSB.js +32 -0
  117. package/dist-cli/chunks/skills-S3Y22TUA.js +50 -0
  118. package/dist-cli/chunks/storage-XUIMJWAJ.js +37 -0
  119. package/dist-cli/chunks/store-JTHEJLAZ.js +4 -0
  120. package/dist-cli/chunks/telemetry-ZYJGD2DB.js +4 -0
  121. package/dist-cli/chunks/timeline-TMPLQPSP.js +24 -0
  122. package/dist-cli/chunks/upgrade-7HDSIM7K.js +7 -0
  123. package/dist-cli/chunks/upload-GMSZPWM6.js +4 -0
  124. package/dist-cli/chunks/version-HOCHZ37L.js +6 -0
  125. package/dist-cli/chunks/web-DG3WBYD3.js +4 -0
  126. package/dist-cli/chunks/what-happened-XFVUTZR7.js +25 -0
  127. package/dist-lib/agent-daemon-client.cjs +468 -0
  128. package/dist-lib/agent-events.cjs +48 -0
  129. package/dist-lib/agent-identity.cjs +53 -0
  130. package/dist-lib/agent-sessions.cjs +725 -0
  131. package/dist-lib/attached-projects.cjs +526 -0
  132. package/dist-lib/auth/shared-session.cjs +195 -0
  133. package/dist-lib/backend-origin.cjs +70 -0
  134. package/dist-lib/beta.cjs +41 -0
  135. package/dist-lib/beta.mjs +13 -0
  136. package/dist-lib/bridge-constants.cjs +161 -0
  137. package/dist-lib/cli-constants.cjs +32 -0
  138. package/dist-lib/config.cjs +96 -0
  139. package/dist-lib/detox/index.cjs +2863 -0
  140. package/dist-lib/detox/jest-preset.cjs +51 -0
  141. package/dist-lib/dev-bundle-resolution.cjs +223 -0
  142. package/dist-lib/home-paths.cjs +421 -0
  143. package/dist-lib/host/bridge-host.cjs +5600 -0
  144. package/dist-lib/host/fetch-proxy-handler.cjs +373 -0
  145. package/dist-lib/host/fetch-proxy-overrides.cjs +63 -0
  146. package/dist-lib/host/fetch-proxy-overrides.mjs +37 -0
  147. package/dist-lib/host/websocket-proxy.cjs +214 -0
  148. package/dist-lib/index.cjs +513 -0
  149. package/dist-lib/jump-to-source-babel.cjs +93 -0
  150. package/dist-lib/menu.cjs +620 -0
  151. package/dist-lib/menu.mjs +581 -0
  152. package/dist-lib/metro.cjs +297 -0
  153. package/dist-lib/profiles.cjs +246 -0
  154. package/dist-lib/public-brand.cjs +39 -0
  155. package/dist-lib/render-mode.cjs +54 -0
  156. package/dist-lib/scripts/dev-server-scanner.cjs +1034 -0
  157. package/dist-lib/sdk.cjs +2333 -0
  158. package/dist-lib/sdk.mjs +2240 -0
  159. package/dist-lib/skills.cjs +23922 -0
  160. package/dist-lib/vite.cjs +358 -0
  161. package/package.json +189 -18
  162. package/scripts/dev-server-scanner.ts +941 -0
  163. package/scripts/optional-demo-registry.ts +113 -0
  164. package/scripts/postinstall.cjs +3 -0
  165. package/scripts/preuninstall.cjs +118 -0
  166. package/skills/contrast/SKILL.md +171 -0
  167. package/skills/rnx-debug/SKILL.md +425 -0
  168. package/skills/rnx-perf/SKILL.md +295 -0
  169. package/skills/rnx-setup/SKILL.md +160 -0
  170. package/skills/rnx-test/SKILL.md +292 -0
  171. package/skills/rnx-visual/SKILL.md +122 -0
  172. package/src/agent-daemon-client.ts +410 -0
  173. package/src/agent-events.ts +71 -0
  174. package/src/agent-identity.ts +23 -0
  175. package/src/agent-prompt.ts +71 -0
  176. package/src/agent-sessions.ts +573 -0
  177. package/src/attached-projects.ts +539 -0
  178. package/src/auth/shared-session.ts +204 -0
  179. package/src/backend-origin.ts +49 -0
  180. package/src/beta.ts +19 -0
  181. package/src/bridge-constants.ts +171 -0
  182. package/src/browser-cache.ts +16 -0
  183. package/src/cli-constants.ts +1 -0
  184. package/src/cli-version.ts +32 -0
  185. package/src/codex-client.ts +215 -0
  186. package/src/config.ts +134 -0
  187. package/src/dev-bundle-resolution.ts +236 -0
  188. package/src/disk-cleanup.ts +466 -0
  189. package/src/home-paths.ts +552 -0
  190. package/src/host/agent-host.ts +666 -0
  191. package/src/host/bridge-host.ts +2626 -0
  192. package/src/host/fetch-proxy-handler.ts +397 -0
  193. package/src/host/fetch-proxy-overrides.ts +51 -0
  194. package/src/host/open-url.ts +89 -0
  195. package/src/host/websocket-proxy.ts +210 -0
  196. package/src/index.ts +12 -705
  197. package/src/jump-to-source-babel.ts +135 -0
  198. package/src/menu.ts +734 -0
  199. package/src/metro-plugin.ts +139 -0
  200. package/src/native-dev-bundle-url.ts +96 -0
  201. package/src/native-seam-manifest.ts +341 -0
  202. package/src/plist.ts +27 -0
  203. package/src/poll-until.ts +92 -0
  204. package/src/profiles.ts +179 -0
  205. package/src/public-brand.ts +9 -0
  206. package/src/render-mode.ts +27 -0
  207. package/src/runtime-assets.ts +228 -0
  208. package/src/runtime-delivery.ts +54 -0
  209. package/src/screenshots/compose.ts +422 -0
  210. package/src/screenshots/frame-compose.ts +454 -0
  211. package/src/screenshots/orchestrate.ts +256 -0
  212. package/src/screenshots/org-deck.ts +91 -0
  213. package/src/screenshots/registry.ts +58 -0
  214. package/src/screenshots/schema.ts +364 -0
  215. package/src/sdk.ts +7 -0
  216. package/src/settings-bundle.ts +418 -0
  217. package/src/skills/builtin/a11y-review.ts +132 -0
  218. package/src/skills/builtin/compat-check.ts +71 -0
  219. package/src/skills/builtin/maestro-test.ts +121 -0
  220. package/src/skills/builtin/perf-profile.ts +90 -0
  221. package/src/skills/builtin/screenshot-all.ts +52 -0
  222. package/src/skills/builtin/visual-diff.ts +100 -0
  223. package/src/skills/registry.ts +107 -0
  224. package/src/skills/types.ts +41 -0
  225. package/src/team-machine-prompt.ts +73 -0
  226. package/src/vite-plugin-one.ts +190 -0
  227. package/src/vite-plugin.ts +1750 -0
  228. package/src/worklets-babel.ts +134 -0
@@ -0,0 +1,397 @@
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 } 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
+ headers.origin = targetUrl.origin
144
+ headers.referer = `${targetUrl.origin}/`
145
+ return headers
146
+ }
147
+
148
+ export function isFetchProxyRequestUrl(rawUrl: string | undefined): boolean {
149
+ return rawUrl?.startsWith('/__fetch-proxy?') || rawUrl?.startsWith('/__proxy?') || false
150
+ }
151
+
152
+ export function isAppApiRequestUrl(rawUrl: string | undefined): boolean {
153
+ if (!rawUrl) return false
154
+ if (rawUrl.startsWith('/__app-api?')) return true
155
+ if (rawUrl.startsWith('/__app-api/')) return true
156
+ return false
157
+ }
158
+
159
+ export async function handleFetchProxyRequest(
160
+ req: IncomingMessage,
161
+ res: ServerResponse,
162
+ ): Promise<void> {
163
+ if (req.method === 'OPTIONS') {
164
+ applyFetchProxyCors(res)
165
+ res.writeHead(204)
166
+ res.end()
167
+ return
168
+ }
169
+
170
+ const params = new URLSearchParams((req.url || '').split('?')[1] || '')
171
+ const targetUrl = params.get('url')
172
+ if (!targetUrl) {
173
+ applyFetchProxyCors(res)
174
+ res.writeHead(400, { 'Content-Type': 'text/plain' })
175
+ res.end('missing url param')
176
+ return
177
+ }
178
+
179
+ let upstreamUrl: URL
180
+ try {
181
+ upstreamUrl = new URL(targetUrl)
182
+ } catch {
183
+ applyFetchProxyCors(res)
184
+ res.writeHead(400, { 'Content-Type': 'text/plain' })
185
+ res.end('invalid url param')
186
+ return
187
+ }
188
+
189
+ let body: Buffer | undefined
190
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
191
+ const chunks: Buffer[] = []
192
+ for await (const chunk of req) {
193
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))
194
+ }
195
+ if (chunks.length > 0) {
196
+ body = Buffer.concat(chunks)
197
+ }
198
+ }
199
+
200
+ const originalMethod = req.method || 'GET'
201
+ const proxy = async (
202
+ targetUrl: URL,
203
+ method: string,
204
+ nextBody: Buffer | undefined,
205
+ redirects: number,
206
+ ): Promise<void> => {
207
+ const transport = targetUrl.protocol === 'https:' ? https : http
208
+ const targetHeaders = buildFetchProxyHeaders(req.headers, targetUrl)
209
+ if (targetUrl.origin !== upstreamUrl.origin) {
210
+ delete targetHeaders.authorization
211
+ delete targetHeaders['proxy-authorization']
212
+ }
213
+ if (method === 'GET' || method === 'HEAD') {
214
+ delete targetHeaders['content-length']
215
+ delete targetHeaders['content-type']
216
+ } else if (nextBody) {
217
+ targetHeaders['content-length'] = String(nextBody.byteLength)
218
+ }
219
+
220
+ const proxyRes = await new Promise<IncomingMessage>((resolve, reject) => {
221
+ const proxyReq = transport.request(
222
+ {
223
+ hostname: targetUrl.hostname,
224
+ port: targetUrl.port || (targetUrl.protocol === 'https:' ? 443 : 80),
225
+ path: targetUrl.pathname + targetUrl.search,
226
+ method,
227
+ headers: targetHeaders,
228
+ ...loopbackTlsOptions(targetUrl),
229
+ },
230
+ resolve,
231
+ )
232
+ const abort = () => proxyReq.destroy(new Error('fetch proxy client disconnected'))
233
+ req.once('aborted', abort)
234
+ proxyReq.once('close', () => req.off('aborted', abort))
235
+ proxyReq.once('error', reject)
236
+ if (nextBody === undefined) proxyReq.end()
237
+ else proxyReq.end(nextBody)
238
+ })
239
+
240
+ const status = proxyRes.statusCode ?? 502
241
+ const location = proxyRes.headers.location
242
+ if (
243
+ location &&
244
+ (status === 301 ||
245
+ status === 302 ||
246
+ status === 303 ||
247
+ status === 307 ||
248
+ status === 308)
249
+ ) {
250
+ if (redirects >= 10) {
251
+ proxyRes.destroy()
252
+ throw new Error('too many redirects')
253
+ }
254
+ const switchToGet =
255
+ ((status === 301 || status === 302) && method === 'POST') ||
256
+ (status === 303 && method !== 'GET' && method !== 'HEAD')
257
+ const redirectUrl = new URL(location, targetUrl)
258
+ proxyRes.resume()
259
+ await finished(proxyRes)
260
+ await proxy(
261
+ redirectUrl,
262
+ switchToGet ? 'GET' : method,
263
+ switchToGet ? undefined : nextBody,
264
+ redirects + 1,
265
+ )
266
+ return
267
+ }
268
+
269
+ for (const [key, value] of Object.entries(proxyRes.headers)) {
270
+ const lowerKey = key.toLowerCase()
271
+ if (
272
+ value === undefined ||
273
+ lowerKey === 'set-cookie' ||
274
+ lowerKey === 'connection' ||
275
+ lowerKey === 'keep-alive' ||
276
+ lowerKey === 'proxy-authenticate' ||
277
+ lowerKey === 'proxy-authorization' ||
278
+ lowerKey === 'te' ||
279
+ lowerKey === 'trailer' ||
280
+ lowerKey === 'transfer-encoding' ||
281
+ lowerKey === 'upgrade' ||
282
+ lowerKey.startsWith('access-control-')
283
+ ) {
284
+ continue
285
+ }
286
+ res.setHeader(key, value)
287
+ }
288
+ applyFetchProxyCors(res)
289
+ const setCookie = proxyRes.headers['set-cookie'] ?? []
290
+ if (setCookie.length > 0) {
291
+ res.setHeader(
292
+ 'x-sootsim-set-cookie',
293
+ (Array.isArray(setCookie) ? setCookie : [setCookie]).join(', '),
294
+ )
295
+ }
296
+ res.statusCode = status
297
+ await pipeline(proxyRes, res)
298
+ }
299
+
300
+ try {
301
+ await proxy(upstreamUrl, originalMethod, body, 0)
302
+ } catch (err) {
303
+ if (res.headersSent) {
304
+ res.destroy(err instanceof Error ? err : new Error(String(err)))
305
+ return
306
+ }
307
+ applyFetchProxyCors(res)
308
+ res.writeHead(502, { 'Content-Type': 'text/plain' })
309
+ res.end(`fetch proxy error: ${formatFetchProxyError(upstreamUrl.href, err)}`)
310
+ }
311
+ }
312
+
313
+ export function handleAppApiRequest(req: IncomingMessage, res: ServerResponse): boolean {
314
+ // returns true if it handled the request, false if the caller should fall
315
+ // through. /__app-api requires `origin` query param for stateless mode;
316
+ // without it we return false so the caller decides what to do (the vite
317
+ // middleware uses its own out-of-band origin registry; the daemon has
318
+ // no such registry so it returns 400).
319
+
320
+ const reqUrl = req.url || ''
321
+
322
+ let targetPath = ''
323
+ let targetOrigin = ''
324
+ if (reqUrl.startsWith('/__app-api?')) {
325
+ const parsed = new URL(reqUrl, 'http://sootsim.local')
326
+ targetPath = parsed.searchParams.get('path') || ''
327
+ targetOrigin = parsed.searchParams.get('origin')?.trim() || ''
328
+ } else if (reqUrl.startsWith('/__app-api/')) {
329
+ targetPath = reqUrl.slice('/__app-api'.length)
330
+ } else {
331
+ return false
332
+ }
333
+
334
+ if (!targetOrigin) {
335
+ res.writeHead(400, { 'Content-Type': 'text/plain' })
336
+ res.end('app-api: missing origin query param')
337
+ return true
338
+ }
339
+
340
+ if (req.method === 'OPTIONS') {
341
+ res.writeHead(204, {
342
+ 'Access-Control-Allow-Origin': (req.headers.origin as string) || '*',
343
+ 'Access-Control-Allow-Methods': 'GET,POST,PUT,PATCH,DELETE,OPTIONS',
344
+ 'Access-Control-Allow-Headers':
345
+ (req.headers['access-control-request-headers'] as string) || '*',
346
+ 'Access-Control-Allow-Credentials': 'true',
347
+ 'Access-Control-Max-Age': '86400',
348
+ })
349
+ res.end()
350
+ return true
351
+ }
352
+
353
+ let targetUrl: URL
354
+ try {
355
+ targetUrl = new URL(targetPath, targetOrigin)
356
+ } catch {
357
+ res.writeHead(400, { 'Content-Type': 'text/plain' })
358
+ res.end('app-api: invalid origin or path')
359
+ return true
360
+ }
361
+
362
+ const transport = targetUrl.protocol === 'https:' ? https : http
363
+
364
+ const fwdHeaders = buildAppApiProxyHeaders(req.headers, targetUrl)
365
+
366
+ const proxyReq = transport.request(
367
+ {
368
+ hostname: targetUrl.hostname,
369
+ port: targetUrl.port || (targetUrl.protocol === 'https:' ? 443 : 80),
370
+ path: targetUrl.pathname + targetUrl.search,
371
+ method: req.method,
372
+ headers: fwdHeaders,
373
+ ...loopbackTlsOptions(targetUrl),
374
+ },
375
+ (proxyRes) => {
376
+ const exposedHeaders = Object.keys(proxyRes.headers)
377
+ .filter((name) => {
378
+ const lower = name.toLowerCase()
379
+ return !lower.startsWith('access-control-') && lower !== 'set-cookie'
380
+ })
381
+ .join(', ')
382
+ res.writeHead(proxyRes.statusCode ?? 502, {
383
+ ...proxyRes.headers,
384
+ 'access-control-allow-origin': (req.headers.origin as string) || '*',
385
+ 'access-control-allow-credentials': 'true',
386
+ 'access-control-expose-headers': exposedHeaders,
387
+ })
388
+ proxyRes.pipe(res)
389
+ },
390
+ )
391
+ proxyReq.on('error', (err) => {
392
+ res.statusCode = 502
393
+ res.end(`app proxy error: ${err.message}`)
394
+ })
395
+ req.pipe(proxyReq)
396
+ return true
397
+ }
@@ -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
+ }