sootsim 0.1.83 → 0.1.84

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 (207) hide show
  1. package/README.md +0 -1
  2. package/detox/colors.ts +54 -0
  3. package/detox/config-loader.ts +135 -0
  4. package/detox/expectations.ts +477 -0
  5. package/detox/gestures.ts +442 -0
  6. package/detox/index.ts +1436 -0
  7. package/detox/jest-environment.ts +86 -0
  8. package/detox/jest-preset.cjs +50 -0
  9. package/detox/matchers.ts +29 -0
  10. package/detox/navigation.ts +43 -0
  11. package/detox/run-test.ts +113 -0
  12. package/detox/screenshots/animated-color-test-rest-norngh.png +0 -0
  13. package/detox/screenshots/color-test-after-drag-norngh.png +0 -0
  14. package/detox/screenshots/color-test-rest-norngh.png +0 -0
  15. package/detox/screenshots/theme-blue-toggle.png +0 -0
  16. package/detox/screenshots/theme-blue.png +0 -0
  17. package/detox/screenshots/theme-red-toggle.png +0 -0
  18. package/detox/screenshots/theme-red.png +0 -0
  19. package/dist-cli/bin.js +3 -3
  20. package/dist-cli/chunks/{agent-MQ7GLVIB.js → agent-2CWD6W6P.js} +2 -2
  21. package/dist-cli/chunks/{agent-wrapper-7KAFDQCN.js → agent-wrapper-5W3LOX6S.js} +2 -2
  22. package/dist-cli/chunks/{assert-TV46GUNU.js → assert-ZOMAMKRT.js} +2 -2
  23. package/dist-cli/chunks/auto-bootstrap-NYYSMTIM.js +2 -0
  24. package/dist-cli/chunks/beta-4K2SQACK.js +2 -0
  25. package/dist-cli/chunks/chunk-3HXQ7MJK.js +79 -0
  26. package/dist-cli/chunks/{chunk-4LS5MZAI.js → chunk-4K7BH2D4.js} +3 -3
  27. package/dist-cli/chunks/{chunk-FJYT7XL2.js → chunk-4OPRODFA.js} +2 -2
  28. package/dist-cli/chunks/{chunk-DP7O5MHK.js → chunk-4OWVPRZV.js} +2 -2
  29. package/dist-cli/chunks/{chunk-PM5NVKLP.js → chunk-5XCXOLG2.js} +2 -2
  30. package/dist-cli/chunks/chunk-67ZZ2CM5.js +1 -0
  31. package/dist-cli/chunks/{chunk-WN7M3QON.js → chunk-73UZXB4B.js} +2 -2
  32. package/dist-cli/chunks/{chunk-5DJXZIFZ.js → chunk-7NWNTUJF.js} +1 -1
  33. package/dist-cli/chunks/{chunk-Y2VJBRSP.js → chunk-7YHDJLO2.js} +1 -1
  34. package/dist-cli/chunks/{chunk-6NN2D4EJ.js → chunk-AJVTY6KY.js} +1 -1
  35. package/dist-cli/chunks/chunk-AWSQUOAS.js +67 -0
  36. package/dist-cli/chunks/{chunk-CJY3AVI7.js → chunk-BCBNVJVG.js} +1 -1
  37. package/dist-cli/chunks/{chunk-OYMFNU3M.js → chunk-BKBL6K2G.js} +1 -1
  38. package/dist-cli/chunks/{chunk-IBNRRAES.js → chunk-C3DPQZ4J.js} +2 -2
  39. package/dist-cli/chunks/chunk-D3ZSBIIY.js +2 -0
  40. package/dist-cli/chunks/{chunk-2AWQ7OB2.js → chunk-D4HUVLZR.js} +1 -1
  41. package/dist-cli/chunks/{chunk-F3HP444U.js → chunk-DUUSJDES.js} +1 -1
  42. package/dist-cli/chunks/{chunk-277XAALA.js → chunk-ELJLF4SG.js} +3 -3
  43. package/dist-cli/chunks/{chunk-RH4F2TF7.js → chunk-EQ7TFQ2F.js} +1 -1
  44. package/dist-cli/chunks/{chunk-HNWEELAE.js → chunk-EQCKGC4B.js} +1 -1
  45. package/dist-cli/chunks/chunk-FUCGLWNN.js +1 -0
  46. package/dist-cli/chunks/{chunk-FRM355UL.js → chunk-HYPJW65U.js} +2 -2
  47. package/dist-cli/chunks/chunk-IILJQCZA.js +2 -0
  48. package/dist-cli/chunks/{chunk-Y4BUVURT.js → chunk-KU6MSPAH.js} +2 -2
  49. package/dist-cli/chunks/{chunk-DM6WT7QM.js → chunk-OOOR7NT2.js} +1 -1
  50. package/dist-cli/chunks/{chunk-HAWOAQAG.js → chunk-P7WDNKOS.js} +3 -3
  51. package/dist-cli/chunks/{chunk-6TNANCQC.js → chunk-PPKKA5VW.js} +2 -2
  52. package/dist-cli/chunks/{chunk-JQ7ZXOXJ.js → chunk-PS2G44GT.js} +2 -2
  53. package/dist-cli/chunks/{chunk-ECJBV65H.js → chunk-QMSJR5R2.js} +2 -2
  54. package/dist-cli/chunks/{chunk-J2GYISVJ.js → chunk-RF4R2U46.js} +2 -2
  55. package/dist-cli/chunks/{chunk-VMXWC2JO.js → chunk-RIXUH3NK.js} +2 -2
  56. package/dist-cli/chunks/{chunk-2PY3UZVO.js → chunk-SFGUPL2X.js} +2 -2
  57. package/dist-cli/chunks/{chunk-572VSFNP.js → chunk-SQX5CAYG.js} +1 -1
  58. package/dist-cli/chunks/{chunk-NXATOWWF.js → chunk-SQZAC7C4.js} +1 -1
  59. package/dist-cli/chunks/{chunk-WTKTOL3C.js → chunk-SV7FOGJ3.js} +2 -2
  60. package/dist-cli/chunks/{chunk-JHJNODXN.js → chunk-TK3OJSEO.js} +2 -2
  61. package/dist-cli/chunks/{chunk-KASUZ5XV.js → chunk-TL7SIZ7S.js} +1 -1
  62. package/dist-cli/chunks/{chunk-6XZOEBTZ.js → chunk-V2GQ4WXJ.js} +2 -2
  63. package/dist-cli/chunks/{chunk-IP3QJLRH.js → chunk-VH7F45CN.js} +1 -1
  64. package/dist-cli/chunks/chunk-WNVNU2OW.js +4 -0
  65. package/dist-cli/chunks/{chunk-YUELRHGB.js → chunk-XQ2OBHBE.js} +2 -2
  66. package/dist-cli/chunks/{chunk-CYV6Y6YV.js → chunk-YCIA4BHJ.js} +2 -2
  67. package/dist-cli/chunks/chunk-ZSMMJMPA.js +1 -0
  68. package/dist-cli/chunks/cli-version-QB4VH24H.js +2 -0
  69. package/dist-cli/chunks/{compat-QLLWBTS3.js → compat-FWSEEGEH.js} +3 -3
  70. package/dist-cli/chunks/{config-2DSLDCXV.js → config-CYI2WAGP.js} +2 -2
  71. package/dist-cli/chunks/control-UXY7YQVX.js +2 -0
  72. package/dist-cli/chunks/{cpu-profile-GEIKHCPC.js → cpu-profile-IKAE3KTY.js} +2 -2
  73. package/dist-cli/chunks/{daemon-4EBUFN4D.js → daemon-ZUMF53YB.js} +2 -2
  74. package/dist-cli/chunks/{debug-WGD6XWOF.js → debug-P6KULKKS.js} +3 -3
  75. package/dist-cli/chunks/{detox-LNKGRZU6.js → detox-SPWAZCYG.js} +2 -2
  76. package/dist-cli/chunks/{device-AYKXKVIQ.js → device-JWEPK6I2.js} +2 -2
  77. package/dist-cli/chunks/{diagnose-TMXSDOOC.js → diagnose-IZODTXV2.js} +2 -2
  78. package/dist-cli/chunks/drivers-MK6WJKBC.js +2 -0
  79. package/dist-cli/chunks/{electron-QFPF7TBY.js → electron-R5GP6RVB.js} +3 -3
  80. package/dist-cli/chunks/flow-6O4GEOPJ.js +2 -0
  81. package/dist-cli/chunks/{hints-MXKRR4TG.js → hints-DYDNYX7N.js} +2 -2
  82. package/dist-cli/chunks/{home-paths-REMWQDAO.js → home-paths-GLMX5OKL.js} +2 -2
  83. package/dist-cli/chunks/{inspect-XGSQNFV7.js → inspect-FJOPCTY2.js} +3 -3
  84. package/dist-cli/chunks/install-A3TUGGHN.js +2 -0
  85. package/dist-cli/chunks/{install-desktop-NQG3RZSA.js → install-desktop-YPJZMZM5.js} +3 -3
  86. package/dist-cli/chunks/{keys-5QZWXL3F.js → keys-GSYPHWNY.js} +2 -2
  87. package/dist-cli/chunks/{launch-SBXOZWKO.js → launch-4G2PKW5X.js} +3 -3
  88. package/dist-cli/chunks/{login-EACQXE24.js → login-KJQGHA64.js} +4 -4
  89. package/dist-cli/chunks/{logout-IBQLMUML.js → logout-XM2SYH5C.js} +2 -2
  90. package/dist-cli/chunks/{maestro-LFYXUX7O.js → maestro-EOWGI7DG.js} +2 -2
  91. package/dist-cli/chunks/{preview-U4SBOEGQ.js → preview-F73TKK37.js} +2 -2
  92. package/dist-cli/chunks/{profile-GWS5ECMY.js → profile-22FDKBUO.js} +2 -2
  93. package/dist-cli/chunks/{react-QDHLMVYL.js → react-5L6VPFUP.js} +2 -2
  94. package/dist-cli/chunks/{record-BUEUWPDI.js → record-JZXCQ4IN.js} +2 -2
  95. package/dist-cli/chunks/runtime-EEBX7CFV.js +2 -0
  96. package/dist-cli/chunks/{runtime-delivery-G7L6RVZ7.js → runtime-delivery-LXUM3R4A.js} +2 -2
  97. package/dist-cli/chunks/{screenshot-T2HBA3VI.js → screenshot-HDRRG33Q.js} +2 -2
  98. package/dist-cli/chunks/{screenshot-mode-EG5HMIH3.js → screenshot-mode-WY63LZIX.js} +2 -2
  99. package/dist-cli/chunks/{screenshots-S52AFHTV.js → screenshots-MPV2ENL5.js} +2 -2
  100. package/dist-cli/chunks/{server-MFFVYUGG.js → server-5LBMCJ3G.js} +2 -2
  101. package/dist-cli/chunks/setup-repo-SZSYNKNI.js +2 -0
  102. package/dist-cli/chunks/{skills-HQGWBS2O.js → skills-BQ73YOBF.js} +2 -2
  103. package/dist-cli/chunks/{start-E3DRYY7W.js → start-2WU4W6ZU.js} +4 -4
  104. package/dist-cli/chunks/store-RE45SUBF.js +2 -0
  105. package/dist-cli/chunks/telemetry-DG6GJLCP.js +2 -0
  106. package/dist-cli/chunks/{test-ZY3EF62K.js → test-OVO4CQTG.js} +3 -3
  107. package/dist-cli/chunks/{three-mode-WSPKQCJ5.js → three-mode-BKM3KFM7.js} +2 -2
  108. package/dist-cli/chunks/{timeline-3XAB5EWZ.js → timeline-MDXGEDQL.js} +2 -2
  109. package/dist-cli/chunks/{upgrade-WNENPFM5.js → upgrade-JGQABWVF.js} +2 -2
  110. package/dist-cli/chunks/upload-UJNUA4ZV.js +2 -0
  111. package/dist-cli/chunks/{web-D2AOZY44.js → web-WYFAYQ72.js} +2 -2
  112. package/dist-cli/chunks/{what-happened-F43KNSG6.js → what-happened-PZW2KW6A.js} +2 -2
  113. package/dist-cli/chunks/{whoami-T22VBR7C.js → whoami-7ATWJQS6.js} +2 -2
  114. package/dist-lib/agent-daemon-client.cjs +1 -1
  115. package/dist-lib/agent-events.cjs +1 -1
  116. package/dist-lib/agent-sessions.cjs +1 -1
  117. package/dist-lib/attached-projects.cjs +1 -1
  118. package/dist-lib/auth/shared-session.cjs +1 -1
  119. package/dist-lib/backend-origin.cjs +1 -1
  120. package/dist-lib/beta.cjs +44 -0
  121. package/dist-lib/bridge-constants.cjs +1 -1
  122. package/dist-lib/cli-constants.cjs +1 -1
  123. package/dist-lib/config.cjs +1 -1
  124. package/dist-lib/detox/index.cjs +1770 -0
  125. package/dist-lib/detox/jest-preset.cjs +50 -0
  126. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  127. package/dist-lib/home-paths.cjs +1 -1
  128. package/dist-lib/host/bridge-host.cjs +1 -1
  129. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  130. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  131. package/dist-lib/index.cjs +1 -1
  132. package/dist-lib/metro.cjs +1 -1
  133. package/dist-lib/profiles.cjs +1 -1
  134. package/dist-lib/render-mode.cjs +1 -1
  135. package/dist-lib/scripts/demo-app-registry.cjs +809 -0
  136. package/dist-lib/scripts/dev-server-scanner.cjs +1269 -0
  137. package/dist-lib/skills.cjs +8322 -0
  138. package/dist-lib/vite-base.cjs +3 -3
  139. package/dist-lib/vite.cjs +1 -1
  140. package/package.json +39 -10
  141. package/scripts/demo-app-registry.ts +989 -0
  142. package/scripts/dev-server-scanner.ts +674 -0
  143. package/src/agent-daemon-client.ts +390 -0
  144. package/src/agent-events.ts +71 -0
  145. package/src/agent-prompt.ts +71 -0
  146. package/src/agent-sessions.ts +572 -0
  147. package/src/attached-projects.ts +536 -0
  148. package/src/auth/shared-session.ts +199 -0
  149. package/src/backend-origin.ts +49 -0
  150. package/src/beta.ts +21 -0
  151. package/src/bridge-constants.ts +10 -0
  152. package/src/cli-constants.ts +1 -0
  153. package/src/cli-version.ts +30 -0
  154. package/src/codex-client.ts +215 -0
  155. package/src/config.ts +110 -0
  156. package/src/dev-bundle-resolution.ts +180 -0
  157. package/src/home-paths.ts +382 -0
  158. package/src/host/agent-host.ts +576 -0
  159. package/src/host/bridge-host.ts +2293 -0
  160. package/src/host/fetch-proxy-handler.ts +288 -0
  161. package/src/host/fetch-proxy-overrides.ts +39 -0
  162. package/src/host/open-url.ts +234 -0
  163. package/src/index.ts +9 -0
  164. package/src/metro-plugin.ts +207 -0
  165. package/src/native-dev-bundle-url.ts +62 -0
  166. package/src/native-seam-manifest.ts +313 -0
  167. package/src/profiles.ts +179 -0
  168. package/src/render-mode.ts +27 -0
  169. package/src/runtime-delivery.ts +334 -0
  170. package/src/screenshots/compose.ts +422 -0
  171. package/src/screenshots/frame-compose.ts +438 -0
  172. package/src/screenshots/orchestrate.ts +244 -0
  173. package/src/screenshots/registry.ts +58 -0
  174. package/src/screenshots/schema.ts +364 -0
  175. package/src/skills/builtin/a11y-review.ts +126 -0
  176. package/src/skills/builtin/compat-check.ts +104 -0
  177. package/src/skills/builtin/perf-profile.ts +84 -0
  178. package/src/skills/builtin/screenshot-all.ts +46 -0
  179. package/src/skills/builtin/test-flow.ts +118 -0
  180. package/src/skills/builtin/visual-diff.ts +94 -0
  181. package/src/skills/registry.ts +107 -0
  182. package/src/skills/types.ts +41 -0
  183. package/src/vite-plugin-one.ts +187 -0
  184. package/src/vite-plugin.ts +1381 -0
  185. package/src/worklets-babel.ts +132 -0
  186. package/dist-cli/chunks/auto-bootstrap-FQS4ZD2K.js +0 -2
  187. package/dist-cli/chunks/beta-VG7CDY2U.js +0 -2
  188. package/dist-cli/chunks/chunk-2OIBDYHW.js +0 -1
  189. package/dist-cli/chunks/chunk-6BNLVMXA.js +0 -1
  190. package/dist-cli/chunks/chunk-6XD6CBJM.js +0 -2
  191. package/dist-cli/chunks/chunk-CHQTO426.js +0 -1
  192. package/dist-cli/chunks/chunk-FAPYGVIU.js +0 -4
  193. package/dist-cli/chunks/chunk-PEHFE3LG.js +0 -64
  194. package/dist-cli/chunks/chunk-RXH2SLKF.js +0 -2
  195. package/dist-cli/chunks/chunk-UXQWC5ZR.js +0 -79
  196. package/dist-cli/chunks/chunk-XFQL74PF.js +0 -5
  197. package/dist-cli/chunks/cli-version-PWF6I6LY.js +0 -2
  198. package/dist-cli/chunks/control-UIOXGYXU.js +0 -2
  199. package/dist-cli/chunks/demo-app-registry-G3BDOFWC.js +0 -2
  200. package/dist-cli/chunks/drivers-IDQF34HP.js +0 -2
  201. package/dist-cli/chunks/flow-3JN3Y7RF.js +0 -2
  202. package/dist-cli/chunks/install-2N3YOOSN.js +0 -2
  203. package/dist-cli/chunks/runtime-PVB4VGUH.js +0 -2
  204. package/dist-cli/chunks/setup-repo-YOF7NV5D.js +0 -2
  205. package/dist-cli/chunks/store-MAI6D3UO.js +0 -2
  206. package/dist-cli/chunks/telemetry-RCQKCJTH.js +0 -2
  207. package/dist-cli/chunks/upload-YLJ4RA73.js +0 -2
@@ -0,0 +1,288 @@
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 {
29
+ FETCH_PROXY_BROWSER_USER_AGENT,
30
+ getFetchProxyTargetHeaders,
31
+ } from './fetch-proxy-overrides.ts'
32
+
33
+ const STRIP_FETCH_PROXY_HEADERS = new Set([
34
+ 'host',
35
+ 'origin',
36
+ 'referer',
37
+ 'user-agent',
38
+ 'accept-encoding',
39
+ 'cookie',
40
+ 'connection',
41
+ 'keep-alive',
42
+ 'transfer-encoding',
43
+ 'upgrade',
44
+ 'content-length',
45
+ 'sec-fetch-site',
46
+ 'sec-fetch-mode',
47
+ 'sec-fetch-dest',
48
+ 'sec-ch-ua',
49
+ 'sec-ch-ua-mobile',
50
+ 'sec-ch-ua-platform',
51
+ ])
52
+
53
+ const FETCH_PROXY_CORS_HEADERS: Record<string, string> = {
54
+ 'access-control-allow-origin': '*',
55
+ 'access-control-allow-methods': 'GET,POST,PUT,DELETE,PATCH,OPTIONS',
56
+ 'access-control-allow-headers': '*',
57
+ 'access-control-expose-headers': '*',
58
+ 'access-control-max-age': '3600',
59
+ }
60
+
61
+ function applyFetchProxyCors(res: ServerResponse) {
62
+ for (const [key, value] of Object.entries(FETCH_PROXY_CORS_HEADERS)) {
63
+ res.setHeader(key, value)
64
+ }
65
+ }
66
+
67
+ function formatFetchProxyError(targetUrl: string, err: unknown): string {
68
+ const details: string[] = []
69
+ const error = err as
70
+ | {
71
+ message?: string
72
+ code?: string
73
+ cause?: { message?: string; code?: string }
74
+ }
75
+ | undefined
76
+
77
+ if (error?.code) details.push(error.code)
78
+ if (error?.message) details.push(error.message)
79
+ if (error?.cause?.code) details.push(error.cause.code)
80
+ if (error?.cause?.message) details.push(error.cause.message)
81
+
82
+ const uniqueDetails = [...new Set(details.filter(Boolean))]
83
+ const message = uniqueDetails.join(' | ') || String(err)
84
+
85
+ if (targetUrl.includes('stored-in-.env.local')) {
86
+ return `${message} | upstream url still contains placeholder env values`
87
+ }
88
+
89
+ return message
90
+ }
91
+
92
+ export function buildFetchProxyHeaders(
93
+ reqHeaders: Record<string, string | string[] | undefined>,
94
+ targetUrl?: URL,
95
+ ): Record<string, string> {
96
+ const headers: Record<string, string> = {}
97
+ for (const [key, value] of Object.entries(reqHeaders)) {
98
+ if (!value) continue
99
+ if (STRIP_FETCH_PROXY_HEADERS.has(key.toLowerCase())) continue
100
+ headers[key] = Array.isArray(value) ? value.join(', ') : value
101
+ }
102
+ Object.assign(
103
+ headers,
104
+ targetUrl
105
+ ? getFetchProxyTargetHeaders(targetUrl)
106
+ : { 'user-agent': FETCH_PROXY_BROWSER_USER_AGENT },
107
+ )
108
+ return headers
109
+ }
110
+
111
+ export function isFetchProxyRequestUrl(rawUrl: string | undefined): boolean {
112
+ return rawUrl?.startsWith('/__fetch-proxy?') || rawUrl?.startsWith('/__proxy?') || false
113
+ }
114
+
115
+ export function isAppApiRequestUrl(rawUrl: string | undefined): boolean {
116
+ if (!rawUrl) return false
117
+ if (rawUrl.startsWith('/__app-api?')) return true
118
+ if (rawUrl.startsWith('/__app-api/')) return true
119
+ return false
120
+ }
121
+
122
+ export async function handleFetchProxyRequest(
123
+ req: IncomingMessage,
124
+ res: ServerResponse,
125
+ ): Promise<void> {
126
+ if (req.method === 'OPTIONS') {
127
+ applyFetchProxyCors(res)
128
+ res.writeHead(204)
129
+ res.end()
130
+ return
131
+ }
132
+
133
+ const params = new URLSearchParams((req.url || '').split('?')[1] || '')
134
+ const targetUrl = params.get('url')
135
+ if (!targetUrl) {
136
+ applyFetchProxyCors(res)
137
+ res.writeHead(400, { 'Content-Type': 'text/plain' })
138
+ res.end('missing url param')
139
+ return
140
+ }
141
+
142
+ let upstreamUrl: URL
143
+ try {
144
+ upstreamUrl = new URL(targetUrl)
145
+ } catch {
146
+ applyFetchProxyCors(res)
147
+ res.writeHead(400, { 'Content-Type': 'text/plain' })
148
+ res.end('invalid url param')
149
+ return
150
+ }
151
+
152
+ const headers = buildFetchProxyHeaders(req.headers, upstreamUrl)
153
+
154
+ let body: Buffer | undefined
155
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
156
+ const chunks: Buffer[] = []
157
+ for await (const chunk of req) {
158
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))
159
+ }
160
+ if (chunks.length > 0) body = Buffer.concat(chunks)
161
+ }
162
+
163
+ let upstream: Response
164
+ try {
165
+ upstream = await fetch(upstreamUrl.href, {
166
+ method: req.method,
167
+ headers,
168
+ body: body as BodyInit | undefined,
169
+ redirect: 'follow',
170
+ })
171
+ } catch (err) {
172
+ applyFetchProxyCors(res)
173
+ res.writeHead(502, { 'Content-Type': 'text/plain' })
174
+ res.end(`fetch proxy error: ${formatFetchProxyError(upstreamUrl.href, err)}`)
175
+ return
176
+ }
177
+
178
+ upstream.headers.forEach((value, key) => {
179
+ const lowerKey = key.toLowerCase()
180
+ if (
181
+ lowerKey === 'content-encoding' ||
182
+ lowerKey === 'transfer-encoding' ||
183
+ lowerKey === 'content-length' ||
184
+ lowerKey === 'set-cookie' ||
185
+ lowerKey.startsWith('access-control-')
186
+ ) {
187
+ return
188
+ }
189
+ res.setHeader(key, value)
190
+ })
191
+ applyFetchProxyCors(res)
192
+
193
+ const setCookie = upstream.headers.getSetCookie?.() ?? []
194
+ if (setCookie.length > 0) {
195
+ res.setHeader('x-sootsim-set-cookie', setCookie.join(', '))
196
+ }
197
+
198
+ res.statusCode = upstream.status
199
+ const buf = Buffer.from(await upstream.arrayBuffer())
200
+ res.end(buf)
201
+ }
202
+
203
+ export function handleAppApiRequest(req: IncomingMessage, res: ServerResponse): boolean {
204
+ // returns true if it handled the request, false if the caller should fall
205
+ // through. /__app-api requires `origin` query param for stateless mode;
206
+ // without it we return false so the caller decides what to do (the vite
207
+ // middleware uses its own out-of-band origin registry; the daemon has
208
+ // no such registry so it returns 400).
209
+
210
+ const reqUrl = req.url || ''
211
+
212
+ let targetPath = ''
213
+ let targetOrigin = ''
214
+ if (reqUrl.startsWith('/__app-api?')) {
215
+ const parsed = new URL(reqUrl, 'http://sootsim.local')
216
+ targetPath = parsed.searchParams.get('path') || ''
217
+ targetOrigin = parsed.searchParams.get('origin')?.trim() || ''
218
+ } else if (reqUrl.startsWith('/__app-api/')) {
219
+ targetPath = reqUrl.slice('/__app-api'.length)
220
+ } else {
221
+ return false
222
+ }
223
+
224
+ if (!targetOrigin) {
225
+ res.writeHead(400, { 'Content-Type': 'text/plain' })
226
+ res.end('app-api: missing origin query param')
227
+ return true
228
+ }
229
+
230
+ if (req.method === 'OPTIONS') {
231
+ res.writeHead(204, {
232
+ 'Access-Control-Allow-Origin': (req.headers.origin as string) || '*',
233
+ 'Access-Control-Allow-Methods': 'GET,POST,PUT,PATCH,DELETE,OPTIONS',
234
+ 'Access-Control-Allow-Headers':
235
+ (req.headers['access-control-request-headers'] as string) || '*',
236
+ 'Access-Control-Allow-Credentials': 'true',
237
+ 'Access-Control-Max-Age': '86400',
238
+ })
239
+ res.end()
240
+ return true
241
+ }
242
+
243
+ let targetUrl: URL
244
+ try {
245
+ targetUrl = new URL(targetPath, targetOrigin)
246
+ } catch {
247
+ res.writeHead(400, { 'Content-Type': 'text/plain' })
248
+ res.end('app-api: invalid origin or path')
249
+ return true
250
+ }
251
+
252
+ const transport = targetUrl.protocol === 'https:' ? https : http
253
+
254
+ const fwdHeaders = { ...req.headers }
255
+ delete fwdHeaders.host
256
+ fwdHeaders.host = targetUrl.host
257
+
258
+ const proxyReq = transport.request(
259
+ {
260
+ hostname: targetUrl.hostname,
261
+ port: targetUrl.port || (targetUrl.protocol === 'https:' ? 443 : 80),
262
+ path: targetUrl.pathname + targetUrl.search,
263
+ method: req.method,
264
+ headers: fwdHeaders,
265
+ },
266
+ (proxyRes) => {
267
+ const exposedHeaders = Object.keys(proxyRes.headers)
268
+ .filter((name) => {
269
+ const lower = name.toLowerCase()
270
+ return !lower.startsWith('access-control-') && lower !== 'set-cookie'
271
+ })
272
+ .join(', ')
273
+ res.writeHead(proxyRes.statusCode ?? 502, {
274
+ ...proxyRes.headers,
275
+ 'access-control-allow-origin': (req.headers.origin as string) || '*',
276
+ 'access-control-allow-credentials': 'true',
277
+ 'access-control-expose-headers': exposedHeaders,
278
+ })
279
+ proxyRes.pipe(res)
280
+ },
281
+ )
282
+ proxyReq.on('error', (err) => {
283
+ res.statusCode = 502
284
+ res.end(`app proxy error: ${err.message}`)
285
+ })
286
+ req.pipe(proxyReq)
287
+ return true
288
+ }
@@ -0,0 +1,39 @@
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
+ }> = [
14
+ {
15
+ hostSuffix: 'uniswap.org',
16
+ headers: {
17
+ origin: 'https://app.uniswap.org',
18
+ referer: 'https://app.uniswap.org/',
19
+ },
20
+ },
21
+ ]
22
+
23
+ function headerOverridesFor(hostname: string): Record<string, string> {
24
+ const host = hostname.toLowerCase()
25
+ for (const override of HOST_HEADER_OVERRIDES) {
26
+ if (host === override.hostSuffix || host.endsWith(`.${override.hostSuffix}`)) {
27
+ return override.headers
28
+ }
29
+ }
30
+ return {}
31
+ }
32
+
33
+ export function getFetchProxyTargetHeaders(targetUrl: URL): Record<string, string> {
34
+ return {
35
+ 'accept-encoding': 'identity',
36
+ 'user-agent': FETCH_PROXY_BROWSER_USER_AGENT,
37
+ ...headerOverridesFor(targetUrl.hostname),
38
+ }
39
+ }
@@ -0,0 +1,234 @@
1
+ import { execFileSync, spawn } from 'child_process'
2
+ import { existsSync } from 'fs'
3
+ import { homedir } from 'os'
4
+ import { join } from 'path'
5
+
6
+ export type OpenUrlLaunchVia = 'chromium' | 'system'
7
+
8
+ export interface OpenUrlOptions {
9
+ newWindow?: boolean
10
+ detached?: boolean
11
+ background?: boolean
12
+ }
13
+
14
+ export interface OpenUrlCommandOptions extends OpenUrlOptions {
15
+ platform?: NodeJS.Platform
16
+ chromiumBinary?: string | null
17
+ }
18
+
19
+ export interface OpenUrlCommand {
20
+ command: string
21
+ args: string[]
22
+ via: OpenUrlLaunchVia
23
+ target?: string
24
+ }
25
+
26
+ export interface OpenUrlLaunchResult extends OpenUrlCommand {
27
+ pid?: number
28
+ attachUrl: string
29
+ }
30
+
31
+ const MAC_CHROMIUM_CANDIDATES = [
32
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
33
+ '~/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
34
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
35
+ '~/Applications/Chromium.app/Contents/MacOS/Chromium',
36
+ '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
37
+ '~/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
38
+ '/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
39
+ '~/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
40
+ '/Applications/Arc.app/Contents/MacOS/Arc',
41
+ '~/Applications/Arc.app/Contents/MacOS/Arc',
42
+ ]
43
+
44
+ const LINUX_CHROMIUM_CANDIDATES = [
45
+ '/usr/bin/google-chrome',
46
+ '/usr/bin/chromium',
47
+ '/usr/bin/chromium-browser',
48
+ '/usr/bin/microsoft-edge',
49
+ '/usr/bin/brave-browser',
50
+ '/snap/bin/chromium',
51
+ ]
52
+
53
+ const WINDOWS_CHROMIUM_CANDIDATES = [
54
+ 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
55
+ 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
56
+ 'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe',
57
+ 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
58
+ 'C:\\Program Files\\BraveSoftware\\Brave-Browser\\Application\\brave.exe',
59
+ 'C:\\Program Files (x86)\\BraveSoftware\\Brave-Browser\\Application\\brave.exe',
60
+ ]
61
+
62
+ const UNIX_CHROMIUM_COMMANDS = [
63
+ 'google-chrome',
64
+ 'chromium',
65
+ 'chromium-browser',
66
+ 'microsoft-edge',
67
+ 'brave-browser',
68
+ ]
69
+
70
+ const WINDOWS_CHROMIUM_COMMANDS = ['chrome', 'msedge', 'brave']
71
+
72
+ function expandHome(candidate: string): string {
73
+ return candidate.startsWith('~/') ? join(homedir(), candidate.slice(2)) : candidate
74
+ }
75
+
76
+ function firstExisting(candidates: string[]): string | null {
77
+ for (const candidate of candidates) {
78
+ const expanded = expandHome(candidate)
79
+ if (existsSync(expanded)) return expanded
80
+ }
81
+ return null
82
+ }
83
+
84
+ function lookupExecutable(command: string, platform: NodeJS.Platform): string | null {
85
+ try {
86
+ const tool = platform === 'win32' ? 'where' : 'which'
87
+ const out = execFileSync(tool, [command], {
88
+ encoding: 'utf8',
89
+ timeout: 1500,
90
+ stdio: ['ignore', 'pipe', 'ignore'],
91
+ }).trim()
92
+ return out ? out.split(/\r?\n/)[0] : null
93
+ } catch {
94
+ return null
95
+ }
96
+ }
97
+
98
+ export function resolveChromiumBinary(
99
+ platform: NodeJS.Platform = process.platform,
100
+ ): string | null {
101
+ const envCandidate = process.env.CHROME_PATH || process.env.CHROMIUM_PATH
102
+ if (envCandidate && existsSync(envCandidate)) return envCandidate
103
+
104
+ const direct =
105
+ platform === 'darwin'
106
+ ? firstExisting(MAC_CHROMIUM_CANDIDATES)
107
+ : platform === 'win32'
108
+ ? firstExisting(WINDOWS_CHROMIUM_CANDIDATES)
109
+ : firstExisting(LINUX_CHROMIUM_CANDIDATES)
110
+ if (direct) return direct
111
+
112
+ const commands =
113
+ platform === 'win32' ? WINDOWS_CHROMIUM_COMMANDS : UNIX_CHROMIUM_COMMANDS
114
+ for (const command of commands) {
115
+ const found = lookupExecutable(command, platform)
116
+ if (found) return found
117
+ }
118
+
119
+ return null
120
+ }
121
+
122
+ export function buildOpenUrlCommand(
123
+ url: string,
124
+ options: OpenUrlCommandOptions = {},
125
+ ): OpenUrlCommand {
126
+ if (!url) throw new Error('openUrl requires a url')
127
+
128
+ const platform = options.platform ?? process.platform
129
+ if (options.newWindow) {
130
+ return buildChromiumUrlCommand(url, { ...options, platform, newWindow: true })
131
+ }
132
+
133
+ if (platform === 'darwin') {
134
+ return {
135
+ command: 'open',
136
+ args: options.background === false ? [url] : ['-g', url],
137
+ via: 'system',
138
+ }
139
+ }
140
+
141
+ if (platform === 'win32') {
142
+ return {
143
+ command: 'cmd',
144
+ args: ['/c', 'start', '', url],
145
+ via: 'system',
146
+ }
147
+ }
148
+
149
+ return {
150
+ command: 'xdg-open',
151
+ args: [url],
152
+ via: 'system',
153
+ }
154
+ }
155
+
156
+ export function buildChromiumUrlCommand(
157
+ url: string,
158
+ options: OpenUrlCommandOptions = {},
159
+ ): OpenUrlCommand {
160
+ if (!url) throw new Error('openUrl requires a url')
161
+
162
+ const platform = options.platform ?? process.platform
163
+ const chromiumBinary =
164
+ 'chromiumBinary' in options ? options.chromiumBinary : resolveChromiumBinary(platform)
165
+ if (!chromiumBinary) {
166
+ throw new Error('browser launch requires Chrome, Chromium, Edge, Brave, or Arc')
167
+ }
168
+
169
+ // the chromium driver always launches a visible system browser window.
170
+ // headless is intentionally NOT supported here: bare `chrome --headless=new
171
+ // <url>` renders the page once and exits — nothing keeps the process (and
172
+ // therefore the sim) alive without an attached CDP client. headless
173
+ // automation is the playwright driver's job. see chromium.ts `launch`.
174
+ const args: string[] = []
175
+ if (options.newWindow !== false) {
176
+ args.push('--new-window')
177
+ }
178
+ args.push(url)
179
+
180
+ return {
181
+ command: chromiumBinary,
182
+ args,
183
+ via: 'chromium',
184
+ target: chromiumBinary,
185
+ }
186
+ }
187
+
188
+ async function spawnLaunch(
189
+ command: string,
190
+ args: string[],
191
+ detached: boolean,
192
+ ): Promise<number | undefined> {
193
+ return new Promise((resolve, reject) => {
194
+ const child = spawn(command, args, {
195
+ detached,
196
+ stdio: 'ignore',
197
+ })
198
+ child.once('error', reject)
199
+ child.once('spawn', () => {
200
+ if (detached) child.unref()
201
+ resolve(child.pid)
202
+ })
203
+ })
204
+ }
205
+
206
+ export async function launchUrl(
207
+ url: string,
208
+ options: OpenUrlOptions = {},
209
+ ): Promise<OpenUrlLaunchResult> {
210
+ const command = buildOpenUrlCommand(url, options)
211
+ const pid = await spawnLaunch(command.command, command.args, options.detached ?? true)
212
+ return {
213
+ ...command,
214
+ pid,
215
+ attachUrl: url,
216
+ }
217
+ }
218
+
219
+ export async function launchChromiumUrl(
220
+ url: string,
221
+ options: OpenUrlCommandOptions = {},
222
+ ): Promise<OpenUrlLaunchResult> {
223
+ const command = buildChromiumUrlCommand(url, options)
224
+ const pid = await spawnLaunch(command.command, command.args, options.detached ?? true)
225
+ return {
226
+ ...command,
227
+ pid,
228
+ attachUrl: url,
229
+ }
230
+ }
231
+
232
+ export async function openUrl(url: string, options: OpenUrlOptions = {}): Promise<void> {
233
+ await launchUrl(url, options)
234
+ }
package/src/index.ts ADDED
@@ -0,0 +1,9 @@
1
+ // sootsim public package — CLI + framework integrations.
2
+ //
3
+ // the bridge client and plugins live here. the canvas rendering engine lives
4
+ // in the private `sootsim-engine` workspace package.
5
+
6
+ export { sootsimPlugin } from './vite-plugin-one'
7
+ export { default as metroPlugin } from './metro-plugin'
8
+ export { DEFAULT_SOOTSIM_BRIDGE_PORT } from './bridge-constants'
9
+ export { DEFAULT_SOOTSIM_SHELL_URL } from './cli-constants'