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.
- package/LICENSE +21 -0
- package/README.md +512 -0
- package/detox/colors.ts +54 -0
- package/detox/config-loader.ts +135 -0
- package/detox/element-types.ts +39 -0
- package/detox/expectations.ts +537 -0
- package/detox/gestures.ts +480 -0
- package/detox/index.ts +2118 -0
- package/detox/jest-preset.cjs +51 -0
- package/detox/jest-setup-after-env.cjs +18 -0
- package/detox/matchers.ts +30 -0
- package/detox/motion-change.cjs +109 -0
- package/detox/motion-change.d.cts +35 -0
- package/detox/navigation.ts +43 -0
- package/detox/screenshots/animated-color-test-rest-norngh.png +0 -0
- package/detox/screenshots/color-test-after-drag-norngh.png +0 -0
- package/detox/screenshots/color-test-rest-norngh.png +0 -0
- package/detox/screenshots/theme-blue-toggle.png +0 -0
- package/detox/screenshots/theme-blue.png +0 -0
- package/detox/screenshots/theme-red-toggle.png +0 -0
- package/detox/screenshots/theme-red.png +0 -0
- package/dist-cli/bin.js +15 -0
- package/dist-cli/chunks/agent-XZ2KTPCU.js +48 -0
- package/dist-cli/chunks/agent-wrapper-JJYYW2WH.js +17 -0
- package/dist-cli/chunks/app-fonts-IXRNQG6B.js +4 -0
- package/dist-cli/chunks/assert-54T5SK5F.js +49 -0
- package/dist-cli/chunks/auth-FI5UDI45.js +12 -0
- package/dist-cli/chunks/beta-JV6UKADW.js +4 -0
- package/dist-cli/chunks/camera-VL73YIKP.js +22 -0
- package/dist-cli/chunks/chunk-2D2UPBBR.js +4 -0
- package/dist-cli/chunks/chunk-2YR5BGA5.js +5 -0
- package/dist-cli/chunks/chunk-3NV2NCNX.js +4 -0
- package/dist-cli/chunks/chunk-46EUUFJ5.js +4 -0
- package/dist-cli/chunks/chunk-4NPPOV2N.js +5 -0
- package/dist-cli/chunks/chunk-5DHC6KHQ.js +7 -0
- package/dist-cli/chunks/chunk-5TEF3ET3.js +74 -0
- package/dist-cli/chunks/chunk-5TPRP5QT.js +4 -0
- package/dist-cli/chunks/chunk-5YJCOWCH.js +4 -0
- package/dist-cli/chunks/chunk-7GN3LVWB.js +4 -0
- package/dist-cli/chunks/chunk-BBULZ7CG.js +568 -0
- package/dist-cli/chunks/chunk-BTWORNNG.js +71 -0
- package/dist-cli/chunks/chunk-D4FFVGI5.js +5 -0
- package/dist-cli/chunks/chunk-DCEMHR2Y.js +4 -0
- package/dist-cli/chunks/chunk-DZS6WPUI.js +4 -0
- package/dist-cli/chunks/chunk-F5ZRSS3C.js +3 -0
- package/dist-cli/chunks/chunk-FSUYIVJ6.js +9 -0
- package/dist-cli/chunks/chunk-G2WW6L2C.js +23 -0
- package/dist-cli/chunks/chunk-GASE6UBA.js +4 -0
- package/dist-cli/chunks/chunk-GGRX24GF.js +142 -0
- package/dist-cli/chunks/chunk-HI5TFJWN.js +5 -0
- package/dist-cli/chunks/chunk-IJ5CAZZC.js +5 -0
- package/dist-cli/chunks/chunk-IJO63TDP.js +4 -0
- package/dist-cli/chunks/chunk-KTHV3RUS.js +26 -0
- package/dist-cli/chunks/chunk-LF2ZVT7O.js +6 -0
- package/dist-cli/chunks/chunk-MJRLLB4R.js +38 -0
- package/dist-cli/chunks/chunk-NFK7T35W.js +4 -0
- package/dist-cli/chunks/chunk-NMF2ZMZQ.js +15 -0
- package/dist-cli/chunks/chunk-OHAZNXLK.js +4 -0
- package/dist-cli/chunks/chunk-OVFJFXUD.js +101 -0
- package/dist-cli/chunks/chunk-OZSSI4WN.js +28 -0
- package/dist-cli/chunks/chunk-QKDWYITG.js +25 -0
- package/dist-cli/chunks/chunk-RSZWCKNT.js +30 -0
- package/dist-cli/chunks/chunk-RTN5C5RL.js +4 -0
- package/dist-cli/chunks/chunk-TIVZIMMW.js +4 -0
- package/dist-cli/chunks/chunk-TZFFR3SD.js +7 -0
- package/dist-cli/chunks/chunk-UC6U3MML.js +5 -0
- package/dist-cli/chunks/chunk-VFCMSYZK.js +6 -0
- package/dist-cli/chunks/chunk-VNQEB4L7.js +5 -0
- package/dist-cli/chunks/chunk-VZXWHRUZ.js +983 -0
- package/dist-cli/chunks/chunk-W6K4EFPH.js +107 -0
- package/dist-cli/chunks/chunk-WEXDAC74.js +4 -0
- package/dist-cli/chunks/chunk-WF3T4SVI.js +14 -0
- package/dist-cli/chunks/chunk-WINYQ44O.js +4 -0
- package/dist-cli/chunks/chunk-WMIIKMGK.js +10 -0
- package/dist-cli/chunks/chunk-WUSWBCWA.js +507 -0
- package/dist-cli/chunks/chunk-WWZIXIRD.js +4 -0
- package/dist-cli/chunks/chunk-XEVZYVIW.js +766 -0
- package/dist-cli/chunks/chunk-YDGQTMQL.js +4 -0
- package/dist-cli/chunks/chunk-YIFT42WN.js +4 -0
- package/dist-cli/chunks/chunk-ZMJD5GEC.js +125 -0
- package/dist-cli/chunks/cleanup-P27PA6JI.js +25 -0
- package/dist-cli/chunks/cli-version-WWLPBDQ7.js +4 -0
- package/dist-cli/chunks/compat-ZD65FED3.js +20 -0
- package/dist-cli/chunks/config-XMJRNM2A.js +13 -0
- package/dist-cli/chunks/control-KMIQT3QP.js +4 -0
- package/dist-cli/chunks/daemon-G2ME7NLB.js +4 -0
- package/dist-cli/chunks/debug-PT4HOP7N.js +200 -0
- package/dist-cli/chunks/desktop-S3FG72AK.js +31 -0
- package/dist-cli/chunks/detox-B3D4IFCN.js +52 -0
- package/dist-cli/chunks/device-XBNDSB2R.js +18 -0
- package/dist-cli/chunks/diagnose-HMQXJE5N.js +43 -0
- package/dist-cli/chunks/disk-cleanup-BLCZ5BSZ.js +4 -0
- package/dist-cli/chunks/drivers-LDECZGP2.js +4 -0
- package/dist-cli/chunks/film-BJGTBYZB.js +74 -0
- package/dist-cli/chunks/flow-UEQNVTU7.js +4 -0
- package/dist-cli/chunks/help-T5FYSVGB.js +4 -0
- package/dist-cli/chunks/hidden-runtime-alias-ANOYADHM.js +4 -0
- package/dist-cli/chunks/home-paths-GT3LFNOR.js +4 -0
- package/dist-cli/chunks/inspect-ZA6XF5LD.js +4 -0
- package/dist-cli/chunks/install-desktop-TIMUDHPL.js +4 -0
- package/dist-cli/chunks/login-FJ737MWG.js +28 -0
- package/dist-cli/chunks/logout-ZCNMMHMY.js +4 -0
- package/dist-cli/chunks/maestro-SZTNKLDF.js +104 -0
- package/dist-cli/chunks/maestro-generate-DCFAIZ4H.js +36 -0
- package/dist-cli/chunks/mode-GRMQCRXR.js +26 -0
- package/dist-cli/chunks/optional-demo-registry-W36EWFFB.js +4 -0
- package/dist-cli/chunks/perf-QYBAAUZG.js +48 -0
- package/dist-cli/chunks/permissions-3QCQ6VF4.js +13 -0
- package/dist-cli/chunks/record-QPPC2S4E.js +72 -0
- package/dist-cli/chunks/report-issue-7NMFP4HK.js +29 -0
- package/dist-cli/chunks/runtime-XOAXMSTU.js +4 -0
- package/dist-cli/chunks/screenshot-command-67AECJFB.js +159 -0
- package/dist-cli/chunks/screenshot-layers-ASWBYPJL.js +20 -0
- package/dist-cli/chunks/screenshots-capture-PXA3HFQK.js +42 -0
- package/dist-cli/chunks/serve-BI2NBAXG.js +44 -0
- package/dist-cli/chunks/setup-7DWPMRSB.js +32 -0
- package/dist-cli/chunks/skills-S3Y22TUA.js +50 -0
- package/dist-cli/chunks/storage-XUIMJWAJ.js +37 -0
- package/dist-cli/chunks/store-JTHEJLAZ.js +4 -0
- package/dist-cli/chunks/telemetry-ZYJGD2DB.js +4 -0
- package/dist-cli/chunks/timeline-TMPLQPSP.js +24 -0
- package/dist-cli/chunks/upgrade-7HDSIM7K.js +7 -0
- package/dist-cli/chunks/upload-GMSZPWM6.js +4 -0
- package/dist-cli/chunks/version-HOCHZ37L.js +6 -0
- package/dist-cli/chunks/web-DG3WBYD3.js +4 -0
- package/dist-cli/chunks/what-happened-XFVUTZR7.js +25 -0
- package/dist-lib/agent-daemon-client.cjs +468 -0
- package/dist-lib/agent-events.cjs +48 -0
- package/dist-lib/agent-identity.cjs +53 -0
- package/dist-lib/agent-sessions.cjs +725 -0
- package/dist-lib/attached-projects.cjs +526 -0
- package/dist-lib/auth/shared-session.cjs +195 -0
- package/dist-lib/backend-origin.cjs +70 -0
- package/dist-lib/beta.cjs +41 -0
- package/dist-lib/beta.mjs +13 -0
- package/dist-lib/bridge-constants.cjs +161 -0
- package/dist-lib/cli-constants.cjs +32 -0
- package/dist-lib/config.cjs +96 -0
- package/dist-lib/detox/index.cjs +2863 -0
- package/dist-lib/detox/jest-preset.cjs +51 -0
- package/dist-lib/dev-bundle-resolution.cjs +223 -0
- package/dist-lib/home-paths.cjs +421 -0
- package/dist-lib/host/bridge-host.cjs +5600 -0
- package/dist-lib/host/fetch-proxy-handler.cjs +373 -0
- package/dist-lib/host/fetch-proxy-overrides.cjs +63 -0
- package/dist-lib/host/fetch-proxy-overrides.mjs +37 -0
- package/dist-lib/host/websocket-proxy.cjs +214 -0
- package/dist-lib/index.cjs +513 -0
- package/dist-lib/jump-to-source-babel.cjs +93 -0
- package/dist-lib/menu.cjs +620 -0
- package/dist-lib/menu.mjs +581 -0
- package/dist-lib/metro.cjs +297 -0
- package/dist-lib/profiles.cjs +246 -0
- package/dist-lib/public-brand.cjs +39 -0
- package/dist-lib/render-mode.cjs +54 -0
- package/dist-lib/scripts/dev-server-scanner.cjs +1034 -0
- package/dist-lib/sdk.cjs +2333 -0
- package/dist-lib/sdk.mjs +2240 -0
- package/dist-lib/skills.cjs +23922 -0
- package/dist-lib/vite.cjs +358 -0
- package/package.json +189 -18
- package/scripts/dev-server-scanner.ts +941 -0
- package/scripts/optional-demo-registry.ts +113 -0
- package/scripts/postinstall.cjs +3 -0
- package/scripts/preuninstall.cjs +118 -0
- package/skills/contrast/SKILL.md +171 -0
- package/skills/rnx-debug/SKILL.md +425 -0
- package/skills/rnx-perf/SKILL.md +295 -0
- package/skills/rnx-setup/SKILL.md +160 -0
- package/skills/rnx-test/SKILL.md +292 -0
- package/skills/rnx-visual/SKILL.md +122 -0
- package/src/agent-daemon-client.ts +410 -0
- package/src/agent-events.ts +71 -0
- package/src/agent-identity.ts +23 -0
- package/src/agent-prompt.ts +71 -0
- package/src/agent-sessions.ts +573 -0
- package/src/attached-projects.ts +539 -0
- package/src/auth/shared-session.ts +204 -0
- package/src/backend-origin.ts +49 -0
- package/src/beta.ts +19 -0
- package/src/bridge-constants.ts +171 -0
- package/src/browser-cache.ts +16 -0
- package/src/cli-constants.ts +1 -0
- package/src/cli-version.ts +32 -0
- package/src/codex-client.ts +215 -0
- package/src/config.ts +134 -0
- package/src/dev-bundle-resolution.ts +236 -0
- package/src/disk-cleanup.ts +466 -0
- package/src/home-paths.ts +552 -0
- package/src/host/agent-host.ts +666 -0
- package/src/host/bridge-host.ts +2626 -0
- package/src/host/fetch-proxy-handler.ts +397 -0
- package/src/host/fetch-proxy-overrides.ts +51 -0
- package/src/host/open-url.ts +89 -0
- package/src/host/websocket-proxy.ts +210 -0
- package/src/index.ts +12 -705
- package/src/jump-to-source-babel.ts +135 -0
- package/src/menu.ts +734 -0
- package/src/metro-plugin.ts +139 -0
- package/src/native-dev-bundle-url.ts +96 -0
- package/src/native-seam-manifest.ts +341 -0
- package/src/plist.ts +27 -0
- package/src/poll-until.ts +92 -0
- package/src/profiles.ts +179 -0
- package/src/public-brand.ts +9 -0
- package/src/render-mode.ts +27 -0
- package/src/runtime-assets.ts +228 -0
- package/src/runtime-delivery.ts +54 -0
- package/src/screenshots/compose.ts +422 -0
- package/src/screenshots/frame-compose.ts +454 -0
- package/src/screenshots/orchestrate.ts +256 -0
- package/src/screenshots/org-deck.ts +91 -0
- package/src/screenshots/registry.ts +58 -0
- package/src/screenshots/schema.ts +364 -0
- package/src/sdk.ts +7 -0
- package/src/settings-bundle.ts +418 -0
- package/src/skills/builtin/a11y-review.ts +132 -0
- package/src/skills/builtin/compat-check.ts +71 -0
- package/src/skills/builtin/maestro-test.ts +121 -0
- package/src/skills/builtin/perf-profile.ts +90 -0
- package/src/skills/builtin/screenshot-all.ts +52 -0
- package/src/skills/builtin/visual-diff.ts +100 -0
- package/src/skills/registry.ts +107 -0
- package/src/skills/types.ts +41 -0
- package/src/team-machine-prompt.ts +73 -0
- package/src/vite-plugin-one.ts +190 -0
- package/src/vite-plugin.ts +1750 -0
- 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
|
+
}
|