sootsim 0.1.82 → 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.
- package/README.md +0 -1
- package/detox/colors.ts +54 -0
- package/detox/config-loader.ts +135 -0
- package/detox/expectations.ts +477 -0
- package/detox/gestures.ts +442 -0
- package/detox/index.ts +1436 -0
- package/detox/jest-environment.ts +86 -0
- package/detox/jest-preset.cjs +50 -0
- package/detox/matchers.ts +29 -0
- package/detox/navigation.ts +43 -0
- package/detox/run-test.ts +113 -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 +3 -3
- package/dist-cli/chunks/{agent-3C6Z6YXA.js → agent-2CWD6W6P.js} +2 -2
- package/dist-cli/chunks/{agent-wrapper-7Z4UFACX.js → agent-wrapper-5W3LOX6S.js} +2 -2
- package/dist-cli/chunks/{assert-XYBIZRDK.js → assert-ZOMAMKRT.js} +2 -2
- package/dist-cli/chunks/auto-bootstrap-NYYSMTIM.js +2 -0
- package/dist-cli/chunks/beta-4K2SQACK.js +2 -0
- package/dist-cli/chunks/chunk-3HXQ7MJK.js +79 -0
- package/dist-cli/chunks/{chunk-EJGEDUOC.js → chunk-4K7BH2D4.js} +3 -3
- package/dist-cli/chunks/{chunk-2EFQQWEC.js → chunk-4OPRODFA.js} +2 -2
- package/dist-cli/chunks/{chunk-Z6G5SDG7.js → chunk-4OWVPRZV.js} +2 -2
- package/dist-cli/chunks/{chunk-DCFGNIJC.js → chunk-5XCXOLG2.js} +2 -2
- package/dist-cli/chunks/chunk-67ZZ2CM5.js +1 -0
- package/dist-cli/chunks/{chunk-M3OULYY3.js → chunk-73UZXB4B.js} +2 -2
- package/dist-cli/chunks/{chunk-QPDWMYCA.js → chunk-7NWNTUJF.js} +1 -1
- package/dist-cli/chunks/chunk-7YHDJLO2.js +119 -0
- package/dist-cli/chunks/{chunk-EX6IOT23.js → chunk-AJVTY6KY.js} +2 -2
- package/dist-cli/chunks/chunk-AWSQUOAS.js +67 -0
- package/dist-cli/chunks/{chunk-JVNGH5S7.js → chunk-BCBNVJVG.js} +1 -1
- package/dist-cli/chunks/{chunk-WZLKUS54.js → chunk-BKBL6K2G.js} +1 -1
- package/dist-cli/chunks/{chunk-DSYW2NOW.js → chunk-C3DPQZ4J.js} +2 -2
- package/dist-cli/chunks/chunk-D3ZSBIIY.js +2 -0
- package/dist-cli/chunks/{chunk-PYDAVGCZ.js → chunk-D4HUVLZR.js} +1 -1
- package/dist-cli/chunks/{chunk-H6NBDJIO.js → chunk-DUUSJDES.js} +1 -1
- package/dist-cli/chunks/{chunk-BVXP2GDN.js → chunk-ELJLF4SG.js} +3 -3
- package/dist-cli/chunks/{chunk-TR7NIFSL.js → chunk-EQ7TFQ2F.js} +1 -1
- package/dist-cli/chunks/{chunk-UOWBKSSI.js → chunk-EQCKGC4B.js} +1 -1
- package/dist-cli/chunks/chunk-FUCGLWNN.js +1 -0
- package/dist-cli/chunks/{chunk-BISEHRNE.js → chunk-HYPJW65U.js} +2 -2
- package/dist-cli/chunks/chunk-IILJQCZA.js +2 -0
- package/dist-cli/chunks/{chunk-2XULSYS6.js → chunk-KU6MSPAH.js} +2 -2
- package/dist-cli/chunks/{chunk-QTJJHBCI.js → chunk-OOOR7NT2.js} +1 -1
- package/dist-cli/chunks/{chunk-U3XCDQRH.js → chunk-P7WDNKOS.js} +3 -3
- package/dist-cli/chunks/{chunk-C7JOLDDQ.js → chunk-PPKKA5VW.js} +2 -2
- package/dist-cli/chunks/{chunk-JUCV3VHM.js → chunk-PS2G44GT.js} +2 -2
- package/dist-cli/chunks/{chunk-PO64TMRT.js → chunk-QMSJR5R2.js} +2 -2
- package/dist-cli/chunks/{chunk-4QUAOBUB.js → chunk-RF4R2U46.js} +2 -2
- package/dist-cli/chunks/{chunk-D3SM2JYB.js → chunk-RIXUH3NK.js} +2 -2
- package/dist-cli/chunks/{chunk-2JQIKL3B.js → chunk-SFGUPL2X.js} +2 -2
- package/dist-cli/chunks/{chunk-GI5MF6LP.js → chunk-SQX5CAYG.js} +1 -1
- package/dist-cli/chunks/{chunk-Q4JNA5VO.js → chunk-SQZAC7C4.js} +1 -1
- package/dist-cli/chunks/{chunk-M4ERVRM4.js → chunk-SV7FOGJ3.js} +2 -2
- package/dist-cli/chunks/{chunk-ZN2C7V5R.js → chunk-TK3OJSEO.js} +2 -2
- package/dist-cli/chunks/{chunk-7SCQEPXK.js → chunk-TL7SIZ7S.js} +1 -1
- package/dist-cli/chunks/{chunk-IZ2OO47Y.js → chunk-V2GQ4WXJ.js} +2 -2
- package/dist-cli/chunks/{chunk-JUDJXJSE.js → chunk-VH7F45CN.js} +1 -1
- package/dist-cli/chunks/chunk-WNVNU2OW.js +4 -0
- package/dist-cli/chunks/{chunk-O3AOQP3V.js → chunk-XQ2OBHBE.js} +2 -2
- package/dist-cli/chunks/{chunk-MQXYJTXM.js → chunk-YCIA4BHJ.js} +2 -2
- package/dist-cli/chunks/chunk-ZSMMJMPA.js +1 -0
- package/dist-cli/chunks/cli-version-QB4VH24H.js +2 -0
- package/dist-cli/chunks/{compat-2DVSCCR7.js → compat-FWSEEGEH.js} +3 -3
- package/dist-cli/chunks/{config-YDX4Q4XM.js → config-CYI2WAGP.js} +2 -2
- package/dist-cli/chunks/control-UXY7YQVX.js +2 -0
- package/dist-cli/chunks/{cpu-profile-GU62WVZZ.js → cpu-profile-IKAE3KTY.js} +2 -2
- package/dist-cli/chunks/{daemon-V5NLDTSB.js → daemon-ZUMF53YB.js} +2 -2
- package/dist-cli/chunks/{debug-HAOCONNB.js → debug-P6KULKKS.js} +3 -3
- package/dist-cli/chunks/{detox-YLC4DLXB.js → detox-SPWAZCYG.js} +2 -2
- package/dist-cli/chunks/{device-ZQ4DN4H6.js → device-JWEPK6I2.js} +2 -2
- package/dist-cli/chunks/{diagnose-HNUO3Z5F.js → diagnose-IZODTXV2.js} +2 -2
- package/dist-cli/chunks/drivers-MK6WJKBC.js +2 -0
- package/dist-cli/chunks/{electron-ZAASAHSW.js → electron-R5GP6RVB.js} +3 -3
- package/dist-cli/chunks/flow-6O4GEOPJ.js +2 -0
- package/dist-cli/chunks/{hints-BA3GE5W5.js → hints-DYDNYX7N.js} +2 -2
- package/dist-cli/chunks/{home-paths-MQXRHBTW.js → home-paths-GLMX5OKL.js} +2 -2
- package/dist-cli/chunks/{inspect-T4RMS5KX.js → inspect-FJOPCTY2.js} +3 -3
- package/dist-cli/chunks/install-A3TUGGHN.js +2 -0
- package/dist-cli/chunks/{install-desktop-MH26VONS.js → install-desktop-YPJZMZM5.js} +3 -3
- package/dist-cli/chunks/{keys-IELIDRGB.js → keys-GSYPHWNY.js} +2 -2
- package/dist-cli/chunks/{launch-VMT3OWOB.js → launch-4G2PKW5X.js} +3 -3
- package/dist-cli/chunks/{login-VZBANVLU.js → login-KJQGHA64.js} +4 -4
- package/dist-cli/chunks/{logout-GWXBTQ4H.js → logout-XM2SYH5C.js} +2 -2
- package/dist-cli/chunks/{maestro-JYHR4HFR.js → maestro-EOWGI7DG.js} +2 -2
- package/dist-cli/chunks/{preview-RPZ4UQ2B.js → preview-F73TKK37.js} +2 -2
- package/dist-cli/chunks/{profile-7FLDF2AP.js → profile-22FDKBUO.js} +2 -2
- package/dist-cli/chunks/{react-3RC4CNDZ.js → react-5L6VPFUP.js} +2 -2
- package/dist-cli/chunks/record-JZXCQ4IN.js +70 -0
- package/dist-cli/chunks/runtime-EEBX7CFV.js +2 -0
- package/dist-cli/chunks/{runtime-delivery-Z7I2KIRB.js → runtime-delivery-LXUM3R4A.js} +2 -2
- package/dist-cli/chunks/{screenshot-GRCZ6AM4.js → screenshot-HDRRG33Q.js} +2 -2
- package/dist-cli/chunks/{screenshot-mode-E4YHXHH5.js → screenshot-mode-WY63LZIX.js} +2 -2
- package/dist-cli/chunks/{screenshots-7SXMX2AY.js → screenshots-MPV2ENL5.js} +2 -2
- package/dist-cli/chunks/{server-GDJ2TCRV.js → server-5LBMCJ3G.js} +2 -2
- package/dist-cli/chunks/setup-repo-SZSYNKNI.js +2 -0
- package/dist-cli/chunks/{skills-62E7NDRC.js → skills-BQ73YOBF.js} +2 -2
- package/dist-cli/chunks/{start-Y7KR5ZQ3.js → start-2WU4W6ZU.js} +4 -4
- package/dist-cli/chunks/store-RE45SUBF.js +2 -0
- package/dist-cli/chunks/telemetry-DG6GJLCP.js +2 -0
- package/dist-cli/chunks/{test-IYMSUPVC.js → test-OVO4CQTG.js} +3 -3
- package/dist-cli/chunks/{three-mode-QKKXCCC2.js → three-mode-BKM3KFM7.js} +2 -2
- package/dist-cli/chunks/{timeline-PF6NQ7RT.js → timeline-MDXGEDQL.js} +2 -2
- package/dist-cli/chunks/{upgrade-CE2Y3TAN.js → upgrade-JGQABWVF.js} +2 -2
- package/dist-cli/chunks/upload-UJNUA4ZV.js +2 -0
- package/dist-cli/chunks/{web-XEO3ZCPF.js → web-WYFAYQ72.js} +2 -2
- package/dist-cli/chunks/{what-happened-372J7YF7.js → what-happened-PZW2KW6A.js} +2 -2
- package/dist-cli/chunks/{whoami-B4E7KCT5.js → whoami-7ATWJQS6.js} +2 -2
- package/dist-lib/agent-daemon-client.cjs +1 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-sessions.cjs +1 -1
- package/dist-lib/attached-projects.cjs +1 -1
- package/dist-lib/auth/shared-session.cjs +1 -1
- package/dist-lib/backend-origin.cjs +1 -1
- package/dist-lib/beta.cjs +44 -0
- package/dist-lib/bridge-constants.cjs +1 -1
- package/dist-lib/cli-constants.cjs +1 -1
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/detox/index.cjs +1770 -0
- package/dist-lib/detox/jest-preset.cjs +50 -0
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +1 -1
- package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
- package/dist-lib/index.cjs +1 -1
- package/dist-lib/metro.cjs +1 -1
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/render-mode.cjs +1 -1
- package/dist-lib/scripts/demo-app-registry.cjs +809 -0
- package/dist-lib/scripts/dev-server-scanner.cjs +1269 -0
- package/dist-lib/skills.cjs +8322 -0
- package/dist-lib/vite-base.cjs +3 -3
- package/dist-lib/vite.cjs +1 -1
- package/package.json +39 -10
- package/scripts/demo-app-registry.ts +989 -0
- package/scripts/dev-server-scanner.ts +674 -0
- package/src/agent-daemon-client.ts +390 -0
- package/src/agent-events.ts +71 -0
- package/src/agent-prompt.ts +71 -0
- package/src/agent-sessions.ts +572 -0
- package/src/attached-projects.ts +536 -0
- package/src/auth/shared-session.ts +199 -0
- package/src/backend-origin.ts +49 -0
- package/src/beta.ts +21 -0
- package/src/bridge-constants.ts +10 -0
- package/src/cli-constants.ts +1 -0
- package/src/cli-version.ts +30 -0
- package/src/codex-client.ts +215 -0
- package/src/config.ts +110 -0
- package/src/dev-bundle-resolution.ts +180 -0
- package/src/home-paths.ts +382 -0
- package/src/host/agent-host.ts +576 -0
- package/src/host/bridge-host.ts +2293 -0
- package/src/host/fetch-proxy-handler.ts +288 -0
- package/src/host/fetch-proxy-overrides.ts +39 -0
- package/src/host/open-url.ts +234 -0
- package/src/index.ts +9 -0
- package/src/metro-plugin.ts +207 -0
- package/src/native-dev-bundle-url.ts +62 -0
- package/src/native-seam-manifest.ts +313 -0
- package/src/profiles.ts +179 -0
- package/src/render-mode.ts +27 -0
- package/src/runtime-delivery.ts +334 -0
- package/src/screenshots/compose.ts +422 -0
- package/src/screenshots/frame-compose.ts +438 -0
- package/src/screenshots/orchestrate.ts +244 -0
- package/src/screenshots/registry.ts +58 -0
- package/src/screenshots/schema.ts +364 -0
- package/src/skills/builtin/a11y-review.ts +126 -0
- package/src/skills/builtin/compat-check.ts +104 -0
- package/src/skills/builtin/perf-profile.ts +84 -0
- package/src/skills/builtin/screenshot-all.ts +46 -0
- package/src/skills/builtin/test-flow.ts +118 -0
- package/src/skills/builtin/visual-diff.ts +94 -0
- package/src/skills/registry.ts +107 -0
- package/src/skills/types.ts +41 -0
- package/src/vite-plugin-one.ts +187 -0
- package/src/vite-plugin.ts +1381 -0
- package/src/worklets-babel.ts +132 -0
- package/dist-cli/chunks/auto-bootstrap-D2EQVL7R.js +0 -2
- package/dist-cli/chunks/beta-VHPXECZY.js +0 -2
- package/dist-cli/chunks/chunk-27HBWBE6.js +0 -4
- package/dist-cli/chunks/chunk-2W5C5J4O.js +0 -64
- package/dist-cli/chunks/chunk-3OH4VCJA.js +0 -1
- package/dist-cli/chunks/chunk-45HLFQRI.js +0 -2
- package/dist-cli/chunks/chunk-7YLCK5HG.js +0 -5
- package/dist-cli/chunks/chunk-BRDUKIZI.js +0 -119
- package/dist-cli/chunks/chunk-GADW2Q5S.js +0 -1
- package/dist-cli/chunks/chunk-HST43CVE.js +0 -2
- package/dist-cli/chunks/chunk-QJBQOGTK.js +0 -73
- package/dist-cli/chunks/chunk-V26REV7G.js +0 -1
- package/dist-cli/chunks/cli-version-WF7T6IKI.js +0 -2
- package/dist-cli/chunks/control-EAK2OPGB.js +0 -2
- package/dist-cli/chunks/demo-app-registry-52A2MI72.js +0 -2
- package/dist-cli/chunks/drivers-B4QPIZ4B.js +0 -2
- package/dist-cli/chunks/flow-PFLHFNVM.js +0 -2
- package/dist-cli/chunks/install-ZCPEMK6U.js +0 -2
- package/dist-cli/chunks/record-CZ33G5FT.js +0 -70
- package/dist-cli/chunks/runtime-AZKHZHJ4.js +0 -2
- package/dist-cli/chunks/setup-repo-3Y2QAZRK.js +0 -2
- package/dist-cli/chunks/store-TDTFZMGA.js +0 -2
- package/dist-cli/chunks/telemetry-G3NIU5NP.js +0 -2
- package/dist-cli/chunks/upload-CLWFS7IL.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'
|