sootsim 0.1.239 → 0.1.241
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/dist-cli/bin.js +4 -4
- package/dist-cli/chunks/{agent-5KVGVFXJ.js → agent-7Z3WG3YJ.js} +2 -2
- package/dist-cli/chunks/{agent-wrapper-YVXMSV3X.js → agent-wrapper-IMTGEJ7T.js} +2 -2
- package/dist-cli/chunks/{app-fonts-IXF6YPQZ.js → app-fonts-7E66PSAV.js} +2 -2
- package/dist-cli/chunks/{assert-FMNM5VTT.js → assert-IEBAQZC7.js} +2 -2
- package/dist-cli/chunks/{auth-LNSDYZU4.js → auth-FRMHPA2X.js} +2 -2
- package/dist-cli/chunks/beta-AMMCLKHD.js +2 -0
- package/dist-cli/chunks/{camera-INMAD3CG.js → camera-O67TQUAH.js} +2 -2
- package/dist-cli/chunks/{chunk-WXYRA447.js → chunk-2DY3JHFK.js} +1 -1
- package/dist-cli/chunks/{chunk-ZL2KSYJP.js → chunk-4JSCYLLH.js} +1 -1
- package/dist-cli/chunks/{chunk-HV2ZRFGP.js → chunk-4V6MFWDY.js} +1 -1
- package/dist-cli/chunks/{chunk-BWKVAEAJ.js → chunk-5COOJUTE.js} +1 -1
- package/dist-cli/chunks/{chunk-XWPQVHOL.js → chunk-5ZNYFTOB.js} +2 -2
- package/dist-cli/chunks/{chunk-QHJLYAOH.js → chunk-7I3PV2RE.js} +2 -2
- package/dist-cli/chunks/{chunk-JBUMYCFR.js → chunk-AROVTS3P.js} +2 -2
- package/dist-cli/chunks/{chunk-LWE27HF7.js → chunk-BIHEU677.js} +3 -3
- package/dist-cli/chunks/{chunk-UZRZGXQ4.js → chunk-CAOW2O5V.js} +2 -2
- package/dist-cli/chunks/{chunk-SOZIIFPY.js → chunk-CSZOZRKB.js} +3 -3
- package/dist-cli/chunks/{chunk-VIM3WDAA.js → chunk-DS2KFLBO.js} +1 -1
- package/dist-cli/chunks/chunk-ECRSTV5C.js +118 -0
- package/dist-cli/chunks/{chunk-NJQSCTFC.js → chunk-EY3ZQWUQ.js} +52 -52
- package/dist-cli/chunks/{chunk-544G2PCM.js → chunk-F5YD4BLA.js} +2 -2
- package/dist-cli/chunks/{chunk-X7JZ4KTU.js → chunk-FR7JCFAR.js} +1 -1
- package/dist-cli/chunks/{chunk-YIS2RXPE.js → chunk-GFZLDVLE.js} +1 -1
- package/dist-cli/chunks/{chunk-PVIUYYIS.js → chunk-GVTSKLLS.js} +2 -2
- package/dist-cli/chunks/{chunk-KCS6OYQV.js → chunk-H7G2QGUG.js} +2 -2
- package/dist-cli/chunks/{chunk-NDU5EJFW.js → chunk-H7SP6J3H.js} +2 -2
- package/dist-cli/chunks/{chunk-CJRALJVV.js → chunk-HNI6U6M5.js} +1 -1
- package/dist-cli/chunks/{chunk-P5JIE7R6.js → chunk-IF5VXL45.js} +2 -2
- package/dist-cli/chunks/{chunk-BKDHODNB.js → chunk-JMTQBHQY.js} +1 -1
- package/dist-cli/chunks/{chunk-CUDTQFGP.js → chunk-JX4N376W.js} +2 -2
- package/dist-cli/chunks/{chunk-FOTAUF7Q.js → chunk-KFI26HW5.js} +1 -1
- package/dist-cli/chunks/chunk-KLXLOMI6.js +1 -0
- package/dist-cli/chunks/{chunk-JPNTLGLA.js → chunk-KN3Q6VSC.js} +2 -2
- package/dist-cli/chunks/{chunk-JOSDTQZ3.js → chunk-LVUOVKJO.js} +2 -2
- package/dist-cli/chunks/{chunk-VBO7OQLR.js → chunk-MCAJBAWD.js} +2 -2
- package/dist-cli/chunks/{chunk-R5I2OBQM.js → chunk-MGLTIS7P.js} +2 -2
- package/dist-cli/chunks/{chunk-SLIYVKRX.js → chunk-PR6AOTP5.js} +1 -1
- package/dist-cli/chunks/{chunk-5VCOEPSJ.js → chunk-PVUUG5YW.js} +2 -2
- package/dist-cli/chunks/{chunk-EQYRXSBE.js → chunk-R3EZVIEM.js} +2 -2
- package/dist-cli/chunks/{chunk-HE5NBBLH.js → chunk-U7XJNNGI.js} +3 -3
- package/dist-cli/chunks/{chunk-ECPEYFHV.js → chunk-UF333CGY.js} +2 -2
- package/dist-cli/chunks/{chunk-QYVGMN3I.js → chunk-VVUANJFA.js} +72 -23
- package/dist-cli/chunks/{chunk-SBSL3DUQ.js → chunk-WFB3ICCN.js} +1 -1
- package/dist-cli/chunks/{chunk-6XF2EHL2.js → chunk-WIHKSZM4.js} +1 -1
- package/dist-cli/chunks/{chunk-MDOTB5DP.js → chunk-WPWLJFKF.js} +2 -2
- package/dist-cli/chunks/{chunk-GI2S43VF.js → chunk-YLWUYESW.js} +1 -1
- package/dist-cli/chunks/{chunk-D4YH5KNQ.js → chunk-YX7S6N42.js} +2 -2
- package/dist-cli/chunks/{chunk-44WCYVFQ.js → chunk-Z72IF5FC.js} +2 -2
- package/dist-cli/chunks/chunk-ZGSGMGE3.js +1 -0
- package/dist-cli/chunks/{chunk-SRMYUHBI.js → chunk-ZUZLQ4ZJ.js} +1 -1
- package/dist-cli/chunks/{chunk-DJIWF2SR.js → chunk-ZYJCVJFP.js} +1 -1
- package/dist-cli/chunks/cli-version-DIYZFJMN.js +2 -0
- package/dist-cli/chunks/{compat-VYOIJUW6.js → compat-ZRUVBIK7.js} +3 -3
- package/dist-cli/chunks/{config-ZJIIQF6K.js → config-VKQDFC5Z.js} +2 -2
- package/dist-cli/chunks/{control-KFAL3LD7.js → control-OYK2J4JW.js} +2 -2
- package/dist-cli/chunks/daemon-YQLY7PVM.js +2 -0
- package/dist-cli/chunks/{debug-RCY6BSLW.js → debug-3WEO7ZRW.js} +3 -3
- package/dist-cli/chunks/{desktop-FRZILY3U.js → desktop-DVOFHJKU.js} +3 -3
- package/dist-cli/chunks/{detox-W2HOZRDP.js → detox-GAGC276D.js} +2 -2
- package/dist-cli/chunks/{device-WRWT7SEE.js → device-256KFP2G.js} +2 -2
- package/dist-cli/chunks/{diagnose-3KSX2NJC.js → diagnose-GYKUU3N5.js} +2 -2
- package/dist-cli/chunks/drivers-FHWFHVQF.js +2 -0
- package/dist-cli/chunks/{film-QN7NUW6Z.js → film-WAV3WPQK.js} +3 -3
- package/dist-cli/chunks/flow-WKPPIDM6.js +2 -0
- package/dist-cli/chunks/help-P4A7NNSC.js +2 -0
- package/dist-cli/chunks/{hidden-runtime-alias-NIH62HCH.js → hidden-runtime-alias-DODMMNOJ.js} +2 -2
- package/dist-cli/chunks/{home-paths-3CPTCZDU.js → home-paths-QU5UX3MT.js} +2 -2
- package/dist-cli/chunks/inspect-NIHY67NU.js +2 -0
- package/dist-cli/chunks/install-desktop-NA53KD6G.js +2 -0
- package/dist-cli/chunks/{login-HQ3356B6.js → login-OURQ3DVM.js} +4 -4
- package/dist-cli/chunks/{logout-LVVXQS5S.js → logout-ZYVJAPBO.js} +2 -2
- package/dist-cli/chunks/{maestro-34Y7GOG5.js → maestro-H2NGXNF4.js} +3 -3
- package/dist-cli/chunks/{maestro-generate-XBIZ7Z6U.js → maestro-generate-DA2IZPK2.js} +3 -3
- package/dist-cli/chunks/{mode-VXZR6I46.js → mode-QTK3ERUW.js} +2 -2
- package/dist-cli/chunks/optional-demo-registry-ZLN23OYV.js +2 -0
- package/dist-cli/chunks/{perf-IBHKDDFN.js → perf-WZ4OPYXV.js} +2 -2
- package/dist-cli/chunks/{record-S5WYHPNR.js → record-ZQSUHQJU.js} +3 -3
- package/dist-cli/chunks/runtime-RM67M6H3.js +2 -0
- package/dist-cli/chunks/{screenshot-command-T65M2SYC.js → screenshot-command-UDJ6TEAM.js} +3 -3
- package/dist-cli/chunks/{screenshots-capture-75AHIYWA.js → screenshots-capture-4FK4NYXG.js} +2 -2
- package/dist-cli/chunks/serve-MCT4HKR7.js +41 -0
- package/dist-cli/chunks/{setup-TLQ2VM3F.js → setup-I2IELNJN.js} +3 -3
- package/dist-cli/chunks/{skills-LZZDB6V5.js → skills-PVXI7LM6.js} +2 -2
- package/dist-cli/chunks/{storage-G2IZ2WAD.js → storage-KZFCVRGD.js} +2 -2
- package/dist-cli/chunks/store-CEMC5MTK.js +2 -0
- package/dist-cli/chunks/telemetry-LFIQWPZD.js +2 -0
- package/dist-cli/chunks/{timeline-L4CFSXX3.js → timeline-YAULXFEC.js} +2 -2
- package/dist-cli/chunks/{upgrade-5USALUAU.js → upgrade-63TVREBM.js} +2 -2
- package/dist-cli/chunks/upload-APW46WK3.js +2 -0
- package/dist-cli/chunks/{version-RQZPUNJO.js → version-JWWEGZS7.js} +2 -2
- package/dist-cli/chunks/{web-YQ6XAB5F.js → web-XO724HJL.js} +2 -2
- package/dist-cli/chunks/{what-happened-JLAM7TBD.js → what-happened-DQ4MXAPJ.js} +2 -2
- package/dist-lib/agent-daemon-client.cjs +1 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-identity.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 +1 -1
- package/dist-lib/beta.mjs +1 -1
- 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 +1 -1
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +77 -33
- package/dist-lib/host/fetch-proxy-handler.cjs +77 -33
- package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
- package/dist-lib/host/websocket-proxy.cjs +1 -1
- package/dist-lib/index.cjs +1 -1
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/menu.cjs +1 -1
- package/dist-lib/menu.mjs +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/dev-server-scanner.cjs +1 -1
- package/dist-lib/sdk.cjs +157 -5
- package/dist-lib/sdk.mjs +156 -5
- package/dist-lib/skills.cjs +221 -37
- package/dist-lib/vite.cjs +1 -1
- package/package.json +1 -1
- package/skills/contrast/SKILL.md +1 -1
- package/skills/sootsim-debug/SKILL.md +21 -12
- package/src/host/fetch-proxy-handler.ts +104 -52
- package/dist-cli/chunks/beta-BH6RPXQU.js +0 -2
- package/dist-cli/chunks/chunk-HJW6SASW.js +0 -118
- package/dist-cli/chunks/chunk-J7SEKMNI.js +0 -1
- package/dist-cli/chunks/chunk-NO6RQXYN.js +0 -1
- package/dist-cli/chunks/cli-version-7KCROGJR.js +0 -2
- package/dist-cli/chunks/daemon-TJXAVVUE.js +0 -2
- package/dist-cli/chunks/drivers-SJOWPOA3.js +0 -2
- package/dist-cli/chunks/flow-Q4XCAPIT.js +0 -2
- package/dist-cli/chunks/help-FRVJH7SB.js +0 -2
- package/dist-cli/chunks/inspect-5ZZN4UJN.js +0 -2
- package/dist-cli/chunks/install-desktop-MIY6DLN6.js +0 -2
- package/dist-cli/chunks/optional-demo-registry-5JSIUIMF.js +0 -2
- package/dist-cli/chunks/runtime-5K5JFYOZ.js +0 -2
- package/dist-cli/chunks/serve-OJVA6PNV.js +0 -41
- package/dist-cli/chunks/store-X6HDJ5SE.js +0 -2
- package/dist-cli/chunks/telemetry-AHMOJS6L.js +0 -2
- package/dist-cli/chunks/upload-OWMPT3Z7.js +0 -2
package/dist-lib/skills.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! sootsim v0.1.
|
|
1
|
+
/*! sootsim v0.1.241 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|
|
@@ -12894,12 +12894,6 @@ function formatFetchProxyError(targetUrl, err) {
|
|
|
12894
12894
|
}
|
|
12895
12895
|
return message;
|
|
12896
12896
|
}
|
|
12897
|
-
function isTextProxyContentType(contentType) {
|
|
12898
|
-
const value = Array.isArray(contentType) ? contentType[0] : contentType;
|
|
12899
|
-
const normalized = value?.split(";", 1)[0]?.trim().toLowerCase();
|
|
12900
|
-
if (!normalized) return false;
|
|
12901
|
-
return normalized.startsWith("text/") || normalized === "application/json" || normalized.endsWith("+json") || normalized === "application/x-www-form-urlencoded" || normalized === "application/xml" || normalized.endsWith("+xml") || normalized === "application/javascript" || normalized === "application/graphql";
|
|
12902
|
-
}
|
|
12903
12897
|
function buildFetchProxyHeaders(reqHeaders, targetUrl) {
|
|
12904
12898
|
const headers = {};
|
|
12905
12899
|
for (const [key, value] of Object.entries(reqHeaders)) {
|
|
@@ -12958,7 +12952,6 @@ async function handleFetchProxyRequest(req, res) {
|
|
|
12958
12952
|
res.end("invalid url param");
|
|
12959
12953
|
return;
|
|
12960
12954
|
}
|
|
12961
|
-
const headers = buildFetchProxyHeaders(req.headers, upstreamUrl);
|
|
12962
12955
|
let body;
|
|
12963
12956
|
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
12964
12957
|
const chunks = [];
|
|
@@ -12966,39 +12959,89 @@ async function handleFetchProxyRequest(req, res) {
|
|
|
12966
12959
|
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
12967
12960
|
}
|
|
12968
12961
|
if (chunks.length > 0) {
|
|
12969
|
-
|
|
12970
|
-
body = isTextProxyContentType(req.headers["content-type"]) ? bytes.toString("utf8") : bytes;
|
|
12962
|
+
body = Buffer.concat(chunks);
|
|
12971
12963
|
}
|
|
12972
12964
|
}
|
|
12973
|
-
|
|
12974
|
-
|
|
12975
|
-
|
|
12976
|
-
|
|
12977
|
-
|
|
12978
|
-
|
|
12979
|
-
|
|
12965
|
+
const originalMethod = req.method || "GET";
|
|
12966
|
+
const proxy = async (targetUrl2, method, nextBody, redirects) => {
|
|
12967
|
+
const transport = targetUrl2.protocol === "https:" ? import_https.default : import_http2.default;
|
|
12968
|
+
const targetHeaders = buildFetchProxyHeaders(req.headers, targetUrl2);
|
|
12969
|
+
if (targetUrl2.origin !== upstreamUrl.origin) {
|
|
12970
|
+
delete targetHeaders.authorization;
|
|
12971
|
+
delete targetHeaders["proxy-authorization"];
|
|
12972
|
+
}
|
|
12973
|
+
if (method === "GET" || method === "HEAD") {
|
|
12974
|
+
delete targetHeaders["content-length"];
|
|
12975
|
+
delete targetHeaders["content-type"];
|
|
12976
|
+
} else if (nextBody) {
|
|
12977
|
+
targetHeaders["content-length"] = String(nextBody.byteLength);
|
|
12978
|
+
}
|
|
12979
|
+
const proxyRes = await new Promise((resolve5, reject) => {
|
|
12980
|
+
const proxyReq = transport.request(
|
|
12981
|
+
{
|
|
12982
|
+
hostname: targetUrl2.hostname,
|
|
12983
|
+
port: targetUrl2.port || (targetUrl2.protocol === "https:" ? 443 : 80),
|
|
12984
|
+
path: targetUrl2.pathname + targetUrl2.search,
|
|
12985
|
+
method,
|
|
12986
|
+
headers: targetHeaders
|
|
12987
|
+
},
|
|
12988
|
+
resolve5
|
|
12989
|
+
);
|
|
12990
|
+
const abort = () => proxyReq.destroy(new Error("fetch proxy client disconnected"));
|
|
12991
|
+
req.once("aborted", abort);
|
|
12992
|
+
proxyReq.once("close", () => req.off("aborted", abort));
|
|
12993
|
+
proxyReq.once("error", reject);
|
|
12994
|
+
if (nextBody === void 0) proxyReq.end();
|
|
12995
|
+
else proxyReq.end(nextBody);
|
|
12980
12996
|
});
|
|
12997
|
+
const status = proxyRes.statusCode ?? 502;
|
|
12998
|
+
const location = proxyRes.headers.location;
|
|
12999
|
+
if (location && (status === 301 || status === 302 || status === 303 || status === 307 || status === 308)) {
|
|
13000
|
+
if (redirects >= 10) {
|
|
13001
|
+
proxyRes.destroy();
|
|
13002
|
+
throw new Error("too many redirects");
|
|
13003
|
+
}
|
|
13004
|
+
const switchToGet = (status === 301 || status === 302) && method === "POST" || status === 303 && method !== "GET" && method !== "HEAD";
|
|
13005
|
+
const redirectUrl = new URL(location, targetUrl2);
|
|
13006
|
+
proxyRes.resume();
|
|
13007
|
+
await (0, import_promises2.finished)(proxyRes);
|
|
13008
|
+
await proxy(
|
|
13009
|
+
redirectUrl,
|
|
13010
|
+
switchToGet ? "GET" : method,
|
|
13011
|
+
switchToGet ? void 0 : nextBody,
|
|
13012
|
+
redirects + 1
|
|
13013
|
+
);
|
|
13014
|
+
return;
|
|
13015
|
+
}
|
|
13016
|
+
for (const [key, value] of Object.entries(proxyRes.headers)) {
|
|
13017
|
+
const lowerKey = key.toLowerCase();
|
|
13018
|
+
if (value === void 0 || lowerKey === "set-cookie" || lowerKey === "connection" || lowerKey === "keep-alive" || lowerKey === "proxy-authenticate" || lowerKey === "proxy-authorization" || lowerKey === "te" || lowerKey === "trailer" || lowerKey === "transfer-encoding" || lowerKey === "upgrade" || lowerKey.startsWith("access-control-")) {
|
|
13019
|
+
continue;
|
|
13020
|
+
}
|
|
13021
|
+
res.setHeader(key, value);
|
|
13022
|
+
}
|
|
13023
|
+
applyFetchProxyCors(res);
|
|
13024
|
+
const setCookie = proxyRes.headers["set-cookie"] ?? [];
|
|
13025
|
+
if (setCookie.length > 0) {
|
|
13026
|
+
res.setHeader(
|
|
13027
|
+
"x-sootsim-set-cookie",
|
|
13028
|
+
(Array.isArray(setCookie) ? setCookie : [setCookie]).join(", ")
|
|
13029
|
+
);
|
|
13030
|
+
}
|
|
13031
|
+
res.statusCode = status;
|
|
13032
|
+
await (0, import_promises2.pipeline)(proxyRes, res);
|
|
13033
|
+
};
|
|
13034
|
+
try {
|
|
13035
|
+
await proxy(upstreamUrl, originalMethod, body, 0);
|
|
12981
13036
|
} catch (err) {
|
|
13037
|
+
if (res.headersSent) {
|
|
13038
|
+
res.destroy(err instanceof Error ? err : new Error(String(err)));
|
|
13039
|
+
return;
|
|
13040
|
+
}
|
|
12982
13041
|
applyFetchProxyCors(res);
|
|
12983
13042
|
res.writeHead(502, { "Content-Type": "text/plain" });
|
|
12984
13043
|
res.end(`fetch proxy error: ${formatFetchProxyError(upstreamUrl.href, err)}`);
|
|
12985
|
-
return;
|
|
12986
|
-
}
|
|
12987
|
-
upstream.headers.forEach((value, key) => {
|
|
12988
|
-
const lowerKey = key.toLowerCase();
|
|
12989
|
-
if (lowerKey === "content-encoding" || lowerKey === "transfer-encoding" || lowerKey === "content-length" || lowerKey === "set-cookie" || lowerKey.startsWith("access-control-")) {
|
|
12990
|
-
return;
|
|
12991
|
-
}
|
|
12992
|
-
res.setHeader(key, value);
|
|
12993
|
-
});
|
|
12994
|
-
applyFetchProxyCors(res);
|
|
12995
|
-
const setCookie = upstream.headers.getSetCookie?.() ?? [];
|
|
12996
|
-
if (setCookie.length > 0) {
|
|
12997
|
-
res.setHeader("x-sootsim-set-cookie", setCookie.join(", "));
|
|
12998
13044
|
}
|
|
12999
|
-
res.statusCode = upstream.status;
|
|
13000
|
-
const buf = Buffer.from(await upstream.arrayBuffer());
|
|
13001
|
-
res.end(buf);
|
|
13002
13045
|
}
|
|
13003
13046
|
function handleAppApiRequest(req, res) {
|
|
13004
13047
|
const reqUrl = req.url || "";
|
|
@@ -13068,12 +13111,13 @@ function handleAppApiRequest(req, res) {
|
|
|
13068
13111
|
req.pipe(proxyReq);
|
|
13069
13112
|
return true;
|
|
13070
13113
|
}
|
|
13071
|
-
var import_http2, import_https, STRIP_FETCH_PROXY_HEADERS, FETCH_PROXY_CORS_HEADERS, APP_API_HEADER_REWRITES;
|
|
13114
|
+
var import_http2, import_https, import_promises2, STRIP_FETCH_PROXY_HEADERS, FETCH_PROXY_CORS_HEADERS, APP_API_HEADER_REWRITES;
|
|
13072
13115
|
var init_fetch_proxy_handler = __esm({
|
|
13073
13116
|
"src/host/fetch-proxy-handler.ts"() {
|
|
13074
13117
|
"use strict";
|
|
13075
13118
|
import_http2 = __toESM(require("http"), 1);
|
|
13076
13119
|
import_https = __toESM(require("https"), 1);
|
|
13120
|
+
import_promises2 = require("stream/promises");
|
|
13077
13121
|
init_fetch_proxy_overrides();
|
|
13078
13122
|
STRIP_FETCH_PROXY_HEADERS = /* @__PURE__ */ new Set([
|
|
13079
13123
|
"host",
|
|
@@ -16385,6 +16429,101 @@ var init_app_state_reset = __esm({
|
|
|
16385
16429
|
});
|
|
16386
16430
|
|
|
16387
16431
|
// cli/commands/inspect/core.ts
|
|
16432
|
+
function scoreLayoutTextContrast(input) {
|
|
16433
|
+
const required = input.fontSize >= 18 || input.fontSize >= 14 && (input.fontWeight ?? 400) >= 700 ? 3 : 4.5;
|
|
16434
|
+
const { normal, background } = input;
|
|
16435
|
+
if (normal.width <= 0 || normal.height <= 0 || normal.width !== background.width || normal.height !== background.height) {
|
|
16436
|
+
return { ratio: null, required, reason: "capture dimensions do not match" };
|
|
16437
|
+
}
|
|
16438
|
+
const left = Math.max(0, Math.floor(input.box.x));
|
|
16439
|
+
const top = Math.max(0, Math.floor(input.box.y));
|
|
16440
|
+
const right = Math.min(normal.width, Math.ceil(input.box.x + input.box.w));
|
|
16441
|
+
const bottom = Math.min(normal.height, Math.ceil(input.box.y + input.box.h));
|
|
16442
|
+
if (right <= left || bottom <= top) {
|
|
16443
|
+
return { ratio: null, required, reason: "text is outside the captured frame" };
|
|
16444
|
+
}
|
|
16445
|
+
const linear = (value) => {
|
|
16446
|
+
const channel = value / 255;
|
|
16447
|
+
return channel <= 0.04045 ? channel / 12.92 : Math.pow((channel + 0.055) / 1.055, 2.4);
|
|
16448
|
+
};
|
|
16449
|
+
const ratioOf = (foreground, behind) => {
|
|
16450
|
+
const foregroundLuminance = 0.2126 * linear(foreground[0]) + 0.7152 * linear(foreground[1]) + 0.0722 * linear(foreground[2]);
|
|
16451
|
+
const backgroundLuminance = 0.2126 * linear(behind[0]) + 0.7152 * linear(behind[1]) + 0.0722 * linear(behind[2]);
|
|
16452
|
+
return (Math.max(foregroundLuminance, backgroundLuminance) + 0.05) / (Math.min(foregroundLuminance, backgroundLuminance) + 0.05);
|
|
16453
|
+
};
|
|
16454
|
+
const hex = (color) => `#${color.slice(0, 3).map((value) => Math.round(value).toString(16).padStart(2, "0")).join("")}`;
|
|
16455
|
+
const area = (right - left) * (bottom - top);
|
|
16456
|
+
const step = Math.max(1, Math.floor(Math.sqrt(area / 12e3)));
|
|
16457
|
+
const offsetAt = (x, y) => (y * normal.width + x) * 4;
|
|
16458
|
+
const deltaAt = (offset) => Math.max(
|
|
16459
|
+
Math.abs(normal.data[offset] - background.data[offset]),
|
|
16460
|
+
Math.abs(normal.data[offset + 1] - background.data[offset + 1]),
|
|
16461
|
+
Math.abs(normal.data[offset + 2] - background.data[offset + 2])
|
|
16462
|
+
);
|
|
16463
|
+
let strongestDelta = 0;
|
|
16464
|
+
let strongestForeground = null;
|
|
16465
|
+
let opaqueBackground = null;
|
|
16466
|
+
for (let y = top; y < bottom; y += step) {
|
|
16467
|
+
for (let x = left; x < right; x += step) {
|
|
16468
|
+
const offset = offsetAt(x, y);
|
|
16469
|
+
if (background.data[offset + 3] < 250) continue;
|
|
16470
|
+
const behind = [
|
|
16471
|
+
background.data[offset],
|
|
16472
|
+
background.data[offset + 1],
|
|
16473
|
+
background.data[offset + 2]
|
|
16474
|
+
];
|
|
16475
|
+
opaqueBackground ??= behind;
|
|
16476
|
+
const delta = deltaAt(offset);
|
|
16477
|
+
if (delta <= strongestDelta) continue;
|
|
16478
|
+
strongestDelta = delta;
|
|
16479
|
+
strongestForeground = [
|
|
16480
|
+
normal.data[offset],
|
|
16481
|
+
normal.data[offset + 1],
|
|
16482
|
+
normal.data[offset + 2]
|
|
16483
|
+
];
|
|
16484
|
+
}
|
|
16485
|
+
}
|
|
16486
|
+
let bestRatio = Infinity;
|
|
16487
|
+
let bestForeground = null;
|
|
16488
|
+
let bestBackground = null;
|
|
16489
|
+
const consider = (foreground, behind) => {
|
|
16490
|
+
const ratio = ratioOf(foreground, behind);
|
|
16491
|
+
if (ratio >= bestRatio) return;
|
|
16492
|
+
bestRatio = ratio;
|
|
16493
|
+
bestForeground = foreground;
|
|
16494
|
+
bestBackground = behind;
|
|
16495
|
+
};
|
|
16496
|
+
if (strongestDelta >= 2 && strongestForeground) {
|
|
16497
|
+
for (let y = top; y < bottom; y += step) {
|
|
16498
|
+
for (let x = left; x < right; x += step) {
|
|
16499
|
+
const offset = offsetAt(x, y);
|
|
16500
|
+
if (background.data[offset + 3] < 250 || deltaAt(offset) < 2) {
|
|
16501
|
+
continue;
|
|
16502
|
+
}
|
|
16503
|
+
consider(strongestForeground, [
|
|
16504
|
+
background.data[offset],
|
|
16505
|
+
background.data[offset + 1],
|
|
16506
|
+
background.data[offset + 2]
|
|
16507
|
+
]);
|
|
16508
|
+
}
|
|
16509
|
+
}
|
|
16510
|
+
} else if (opaqueBackground) {
|
|
16511
|
+
consider(opaqueBackground, opaqueBackground);
|
|
16512
|
+
}
|
|
16513
|
+
if (!bestForeground || !bestBackground || !Number.isFinite(bestRatio)) {
|
|
16514
|
+
return {
|
|
16515
|
+
ratio: null,
|
|
16516
|
+
required,
|
|
16517
|
+
reason: "could not isolate opaque rendered text pixels"
|
|
16518
|
+
};
|
|
16519
|
+
}
|
|
16520
|
+
return {
|
|
16521
|
+
ratio: Math.round(bestRatio * 100) / 100,
|
|
16522
|
+
required,
|
|
16523
|
+
foreground: hex(bestForeground),
|
|
16524
|
+
background: hex(bestBackground)
|
|
16525
|
+
};
|
|
16526
|
+
}
|
|
16388
16527
|
function layoutGetNativeEval(opts = {}) {
|
|
16389
16528
|
const includeStyle = opts.styling === true;
|
|
16390
16529
|
const filter = opts.filter?.trim().toLowerCase() ?? "";
|
|
@@ -16394,6 +16533,7 @@ function layoutGetNativeEval(opts = {}) {
|
|
|
16394
16533
|
const list = await t.listInspectable({})
|
|
16395
16534
|
if (!Array.isArray(list)) return []
|
|
16396
16535
|
const includeStyle = ${JSON.stringify(includeStyle)}
|
|
16536
|
+
const scoreContrast = ${scoreLayoutTextContrast.toString()}
|
|
16397
16537
|
// narrows by ELEMENT, not by style field: fewer rows, full detail on each.
|
|
16398
16538
|
const filter = ${JSON.stringify(filter)}
|
|
16399
16539
|
const matchesFilter = (n, text) => {
|
|
@@ -16438,7 +16578,7 @@ function layoutGetNativeEval(opts = {}) {
|
|
|
16438
16578
|
}
|
|
16439
16579
|
const weightOf = (style) => {
|
|
16440
16580
|
const w = style && style.fontWeight
|
|
16441
|
-
const n = typeof w === 'number' ? w : (typeof w === 'string' ? parseInt(w, 10) : NaN)
|
|
16581
|
+
const n = w === 'bold' ? 700 : (typeof w === 'number' ? w : (typeof w === 'string' ? parseInt(w, 10) : NaN))
|
|
16442
16582
|
return Number.isFinite(n) && n >= 600 ? n : undefined
|
|
16443
16583
|
}
|
|
16444
16584
|
const decoOf = (style) => {
|
|
@@ -16481,7 +16621,7 @@ function layoutGetNativeEval(opts = {}) {
|
|
|
16481
16621
|
return abs.x + layout.width > 0 && abs.y + layout.height > 0 && abs.x < screenW && abs.y < screenH
|
|
16482
16622
|
}
|
|
16483
16623
|
|
|
16484
|
-
|
|
16624
|
+
const elements = list.filter(isVisible).filter((n) => {
|
|
16485
16625
|
const t = typeof n.text === 'string' && n.text.trim() ? n.text.trim() : undefined
|
|
16486
16626
|
return matchesFilter(n, t)
|
|
16487
16627
|
}).map((n) => {
|
|
@@ -16516,7 +16656,7 @@ function layoutGetNativeEval(opts = {}) {
|
|
|
16516
16656
|
...base,
|
|
16517
16657
|
borderRadius: radiusOf(style),
|
|
16518
16658
|
padding: { t: round(pad.top), r: round(pad.right), b: round(pad.bottom), l: round(pad.left) },
|
|
16519
|
-
fontSize: fontSizeOf(style),
|
|
16659
|
+
fontSize: text ? (fontSizeOf(style) || 14) : 0,
|
|
16520
16660
|
color: text ? colorStr(style.color) : undefined,
|
|
16521
16661
|
bg: colorStr(style.backgroundColor),
|
|
16522
16662
|
opacity: opacityOf(style),
|
|
@@ -16527,6 +16667,50 @@ function layoutGetNativeEval(opts = {}) {
|
|
|
16527
16667
|
borderColor: borderWidth ? borderColorOf(style) : undefined,
|
|
16528
16668
|
}
|
|
16529
16669
|
})
|
|
16670
|
+
const textElements = elements.filter((element) => element.text && element.fontSize > 0)
|
|
16671
|
+
if (!includeStyle || textElements.length === 0) return elements
|
|
16672
|
+
|
|
16673
|
+
const screenshot = window.SootSim && window.SootSim.bridges && window.SootSim.bridges.screenshot
|
|
16674
|
+
if (typeof screenshot !== 'function') {
|
|
16675
|
+
throw new Error('styled native layout requires the SootSim capture bridge')
|
|
16676
|
+
}
|
|
16677
|
+
const device = list.find((node) => node && node.device)?.device || {}
|
|
16678
|
+
const width = Math.round(Number(device.width) || window.innerWidth || 0)
|
|
16679
|
+
const height = Math.round(Number(device.height) || window.innerHeight || 0)
|
|
16680
|
+
if (!width || !height) throw new Error('native layout has no capture dimensions')
|
|
16681
|
+
const capture = async (suppressText) => {
|
|
16682
|
+
const dataUrl = await screenshot({
|
|
16683
|
+
format: 'png',
|
|
16684
|
+
outputWidth: width,
|
|
16685
|
+
outputHeight: height,
|
|
16686
|
+
...(suppressText ? { suppressText: true } : {}),
|
|
16687
|
+
})
|
|
16688
|
+
const image = await new Promise((resolve, reject) => {
|
|
16689
|
+
const next = new Image()
|
|
16690
|
+
next.onload = () => resolve(next)
|
|
16691
|
+
next.onerror = () => reject(new Error('could not decode native layout pixels'))
|
|
16692
|
+
next.src = dataUrl
|
|
16693
|
+
})
|
|
16694
|
+
const canvas = document.createElement('canvas')
|
|
16695
|
+
canvas.width = image.naturalWidth
|
|
16696
|
+
canvas.height = image.naturalHeight
|
|
16697
|
+
const context = canvas.getContext('2d', { willReadFrequently: true })
|
|
16698
|
+
if (!context) throw new Error('could not read native layout pixels')
|
|
16699
|
+
context.drawImage(image, 0, 0)
|
|
16700
|
+
return context.getImageData(0, 0, canvas.width, canvas.height)
|
|
16701
|
+
}
|
|
16702
|
+
const normal = await capture(false)
|
|
16703
|
+
const background = await capture(true)
|
|
16704
|
+
for (const element of textElements) {
|
|
16705
|
+
element.contrast = scoreContrast({
|
|
16706
|
+
normal,
|
|
16707
|
+
background,
|
|
16708
|
+
box: element.box,
|
|
16709
|
+
fontSize: element.fontSize,
|
|
16710
|
+
fontWeight: element.fontWeight,
|
|
16711
|
+
})
|
|
16712
|
+
}
|
|
16713
|
+
return elements
|
|
16530
16714
|
})()`;
|
|
16531
16715
|
}
|
|
16532
16716
|
function readyProbeHasContent(probe) {
|
package/dist-lib/vite.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! sootsim v0.1.
|
|
1
|
+
/*! sootsim v0.1.241 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|
package/package.json
CHANGED
package/skills/contrast/SKILL.md
CHANGED
|
@@ -29,7 +29,7 @@ automation, inspecting the render tree, and managing sims.
|
|
|
29
29
|
| `sootsim find` | locate nodes by text / testID / role / type / predicate |
|
|
30
30
|
| `sootsim assert` | run any read verb and convert its output into an exit code |
|
|
31
31
|
| `sootsim screenshot` | capture PNGs and manage App Store screenshot assets |
|
|
32
|
-
| `sootsim get` | read runtime state (tree, a11y, url, count, errors, …) |
|
|
32
|
+
| `sootsim get` | read runtime state (tree, a11y, layout, url, count, errors, …) |
|
|
33
33
|
| `sootsim do` | drive the app (tap, type, scroll, swipe, key, reload, …) |
|
|
34
34
|
| `sootsim wait` | block until the runtime reaches a known state |
|
|
35
35
|
| `sootsim shell` | drive shell state and simulator chrome |
|
|
@@ -239,17 +239,19 @@ watch out for:
|
|
|
239
239
|
## branch — accessibility
|
|
240
240
|
|
|
241
241
|
```sh
|
|
242
|
-
sootsim get a11y #
|
|
243
|
-
sootsim get
|
|
242
|
+
sootsim get a11y # roles, labels, hints, and target geometry
|
|
243
|
+
sootsim get layout --styling # boxes, styles, and native pixel contrast
|
|
244
244
|
sootsim describe --a11y # inline a11y info on every node
|
|
245
245
|
```
|
|
246
246
|
|
|
247
|
-
|
|
247
|
+
native styled layout reads rendered pixels rather than inferring contrast from
|
|
248
|
+
tokens. it compares the normal frame with one CanvasKit capture where text is
|
|
249
|
+
suppressed and adds each text score to the existing layout row and issue list.
|
|
250
|
+
|
|
251
|
+
contrast summary format:
|
|
248
252
|
|
|
249
253
|
```
|
|
250
|
-
|
|
251
|
-
[button] "➜" @(340,280) 20x20 (tap) ⚠ touch target <44×44
|
|
252
|
-
<text> "Sign in" color=#777 bg=#888 ⚠ contrast 1.4:1 (need 4.5:1)
|
|
254
|
+
#loginButton @(24,347) 120x24 font:16 color:#777 contrast:1.4/4.5 FAIL on:#888888 "Sign in"
|
|
253
255
|
```
|
|
254
256
|
|
|
255
257
|
finding offenders fast:
|
|
@@ -266,7 +268,8 @@ what gets checked:
|
|
|
266
268
|
- **missing roles** — `Pressable`/`Touchable` without `accessibilityRole`
|
|
267
269
|
- **missing hints** — complex interactions without `accessibilityHint`
|
|
268
270
|
- **touch target size** — tappable nodes with hitSlop-expanded rect <44×44
|
|
269
|
-
- **contrast** —
|
|
271
|
+
- **contrast** — run `sootsim get layout --styling`; it samples final text and
|
|
272
|
+
text-suppressed background pixels, including gradients and composited opacity
|
|
270
273
|
- **label / visible-text mismatch** — label that reads differently from
|
|
271
274
|
the text VoiceOver would fall back to (often a translation bug)
|
|
272
275
|
- **duplicate labels** — two reachable nodes with identical labels on
|
|
@@ -282,10 +285,13 @@ triage order:
|
|
|
282
285
|
fix → verify loop (the same shape as debugging):
|
|
283
286
|
|
|
284
287
|
```sh
|
|
285
|
-
sootsim get a11y
|
|
288
|
+
sootsim get a11y > before-a11y.txt
|
|
289
|
+
sootsim get layout --styling --json > before-layout.json
|
|
286
290
|
# apply the fix
|
|
287
|
-
sootsim get a11y
|
|
288
|
-
|
|
291
|
+
sootsim get a11y > after-a11y.txt
|
|
292
|
+
sootsim get layout --styling --json > after-layout.json
|
|
293
|
+
diff before-a11y.txt after-a11y.txt
|
|
294
|
+
diff before-layout.json after-layout.json
|
|
289
295
|
```
|
|
290
296
|
|
|
291
297
|
fixing common cases:
|
|
@@ -340,8 +346,11 @@ type names sneak in).
|
|
|
340
346
|
`--visible`, or both.
|
|
341
347
|
- **`perf shell` reports zero frames** — you didn't run `perf shell start`
|
|
342
348
|
first, or the capture window closed before any frame committed.
|
|
343
|
-
- **`get a11y
|
|
344
|
-
|
|
349
|
+
- **`get a11y` returns 100+ items** — narrow with `find --role`,
|
|
350
|
+
`find --pressable`, or `find --no-label`; the a11y tree is descriptive and
|
|
351
|
+
intentionally does not invent an `--issues` grading mode.
|
|
352
|
+
- **styled layout cannot capture pixels** — open a tenant app, dismiss system
|
|
353
|
+
overlays, and let the launch transition finish before retrying.
|
|
345
354
|
- **timeline footer keeps showing 1 error but `get errors` is empty** —
|
|
346
355
|
the footer reads "since last cursor advance"; `get errors` is absolute.
|
|
347
356
|
the error already scrolled out of the absolute ring buffer.
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
|
|
26
26
|
import http, { type IncomingMessage, type ServerResponse } from 'http'
|
|
27
27
|
import https from 'https'
|
|
28
|
+
import { finished, pipeline } from 'stream/promises'
|
|
28
29
|
import {
|
|
29
30
|
FETCH_PROXY_BROWSER_USER_AGENT,
|
|
30
31
|
getFetchProxyTargetHeaders,
|
|
@@ -98,22 +99,6 @@ function formatFetchProxyError(targetUrl: string, err: unknown): string {
|
|
|
98
99
|
return message
|
|
99
100
|
}
|
|
100
101
|
|
|
101
|
-
function isTextProxyContentType(contentType: string | string[] | undefined): boolean {
|
|
102
|
-
const value = Array.isArray(contentType) ? contentType[0] : contentType
|
|
103
|
-
const normalized = value?.split(';', 1)[0]?.trim().toLowerCase()
|
|
104
|
-
if (!normalized) return false
|
|
105
|
-
return (
|
|
106
|
-
normalized.startsWith('text/') ||
|
|
107
|
-
normalized === 'application/json' ||
|
|
108
|
-
normalized.endsWith('+json') ||
|
|
109
|
-
normalized === 'application/x-www-form-urlencoded' ||
|
|
110
|
-
normalized === 'application/xml' ||
|
|
111
|
-
normalized.endsWith('+xml') ||
|
|
112
|
-
normalized === 'application/javascript' ||
|
|
113
|
-
normalized === 'application/graphql'
|
|
114
|
-
)
|
|
115
|
-
}
|
|
116
|
-
|
|
117
102
|
export function buildFetchProxyHeaders(
|
|
118
103
|
reqHeaders: Record<string, string | string[] | undefined>,
|
|
119
104
|
targetUrl?: URL,
|
|
@@ -190,60 +175,127 @@ export async function handleFetchProxyRequest(
|
|
|
190
175
|
return
|
|
191
176
|
}
|
|
192
177
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
let body: BodyInit | undefined
|
|
178
|
+
let body: Buffer | undefined
|
|
196
179
|
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
197
180
|
const chunks: Buffer[] = []
|
|
198
181
|
for await (const chunk of req) {
|
|
199
182
|
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))
|
|
200
183
|
}
|
|
201
184
|
if (chunks.length > 0) {
|
|
202
|
-
|
|
203
|
-
body = isTextProxyContentType(req.headers['content-type'])
|
|
204
|
-
? bytes.toString('utf8')
|
|
205
|
-
: bytes
|
|
185
|
+
body = Buffer.concat(chunks)
|
|
206
186
|
}
|
|
207
187
|
}
|
|
208
188
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
189
|
+
const originalMethod = req.method || 'GET'
|
|
190
|
+
const proxy = async (
|
|
191
|
+
targetUrl: URL,
|
|
192
|
+
method: string,
|
|
193
|
+
nextBody: Buffer | undefined,
|
|
194
|
+
redirects: number,
|
|
195
|
+
): Promise<void> => {
|
|
196
|
+
const transport = targetUrl.protocol === 'https:' ? https : http
|
|
197
|
+
const targetHeaders = buildFetchProxyHeaders(req.headers, targetUrl)
|
|
198
|
+
if (targetUrl.origin !== upstreamUrl.origin) {
|
|
199
|
+
delete targetHeaders.authorization
|
|
200
|
+
delete targetHeaders['proxy-authorization']
|
|
201
|
+
}
|
|
202
|
+
if (method === 'GET' || method === 'HEAD') {
|
|
203
|
+
delete targetHeaders['content-length']
|
|
204
|
+
delete targetHeaders['content-type']
|
|
205
|
+
} else if (nextBody) {
|
|
206
|
+
targetHeaders['content-length'] = String(nextBody.byteLength)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const proxyRes = await new Promise<IncomingMessage>((resolve, reject) => {
|
|
210
|
+
const proxyReq = transport.request(
|
|
211
|
+
{
|
|
212
|
+
hostname: targetUrl.hostname,
|
|
213
|
+
port: targetUrl.port || (targetUrl.protocol === 'https:' ? 443 : 80),
|
|
214
|
+
path: targetUrl.pathname + targetUrl.search,
|
|
215
|
+
method,
|
|
216
|
+
headers: targetHeaders,
|
|
217
|
+
},
|
|
218
|
+
resolve,
|
|
219
|
+
)
|
|
220
|
+
const abort = () => proxyReq.destroy(new Error('fetch proxy client disconnected'))
|
|
221
|
+
req.once('aborted', abort)
|
|
222
|
+
proxyReq.once('close', () => req.off('aborted', abort))
|
|
223
|
+
proxyReq.once('error', reject)
|
|
224
|
+
if (nextBody === undefined) proxyReq.end()
|
|
225
|
+
else proxyReq.end(nextBody)
|
|
216
226
|
})
|
|
217
|
-
} catch (err) {
|
|
218
|
-
applyFetchProxyCors(res)
|
|
219
|
-
res.writeHead(502, { 'Content-Type': 'text/plain' })
|
|
220
|
-
res.end(`fetch proxy error: ${formatFetchProxyError(upstreamUrl.href, err)}`)
|
|
221
|
-
return
|
|
222
|
-
}
|
|
223
227
|
|
|
224
|
-
|
|
225
|
-
const
|
|
228
|
+
const status = proxyRes.statusCode ?? 502
|
|
229
|
+
const location = proxyRes.headers.location
|
|
226
230
|
if (
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
231
|
+
location &&
|
|
232
|
+
(status === 301 ||
|
|
233
|
+
status === 302 ||
|
|
234
|
+
status === 303 ||
|
|
235
|
+
status === 307 ||
|
|
236
|
+
status === 308)
|
|
232
237
|
) {
|
|
238
|
+
if (redirects >= 10) {
|
|
239
|
+
proxyRes.destroy()
|
|
240
|
+
throw new Error('too many redirects')
|
|
241
|
+
}
|
|
242
|
+
const switchToGet =
|
|
243
|
+
((status === 301 || status === 302) && method === 'POST') ||
|
|
244
|
+
(status === 303 && method !== 'GET' && method !== 'HEAD')
|
|
245
|
+
const redirectUrl = new URL(location, targetUrl)
|
|
246
|
+
proxyRes.resume()
|
|
247
|
+
await finished(proxyRes)
|
|
248
|
+
await proxy(
|
|
249
|
+
redirectUrl,
|
|
250
|
+
switchToGet ? 'GET' : method,
|
|
251
|
+
switchToGet ? undefined : nextBody,
|
|
252
|
+
redirects + 1,
|
|
253
|
+
)
|
|
233
254
|
return
|
|
234
255
|
}
|
|
235
|
-
res.setHeader(key, value)
|
|
236
|
-
})
|
|
237
|
-
applyFetchProxyCors(res)
|
|
238
256
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
257
|
+
for (const [key, value] of Object.entries(proxyRes.headers)) {
|
|
258
|
+
const lowerKey = key.toLowerCase()
|
|
259
|
+
if (
|
|
260
|
+
value === undefined ||
|
|
261
|
+
lowerKey === 'set-cookie' ||
|
|
262
|
+
lowerKey === 'connection' ||
|
|
263
|
+
lowerKey === 'keep-alive' ||
|
|
264
|
+
lowerKey === 'proxy-authenticate' ||
|
|
265
|
+
lowerKey === 'proxy-authorization' ||
|
|
266
|
+
lowerKey === 'te' ||
|
|
267
|
+
lowerKey === 'trailer' ||
|
|
268
|
+
lowerKey === 'transfer-encoding' ||
|
|
269
|
+
lowerKey === 'upgrade' ||
|
|
270
|
+
lowerKey.startsWith('access-control-')
|
|
271
|
+
) {
|
|
272
|
+
continue
|
|
273
|
+
}
|
|
274
|
+
res.setHeader(key, value)
|
|
275
|
+
}
|
|
276
|
+
applyFetchProxyCors(res)
|
|
277
|
+
const setCookie = proxyRes.headers['set-cookie'] ?? []
|
|
278
|
+
if (setCookie.length > 0) {
|
|
279
|
+
res.setHeader(
|
|
280
|
+
'x-sootsim-set-cookie',
|
|
281
|
+
(Array.isArray(setCookie) ? setCookie : [setCookie]).join(', '),
|
|
282
|
+
)
|
|
283
|
+
}
|
|
284
|
+
res.statusCode = status
|
|
285
|
+
await pipeline(proxyRes, res)
|
|
242
286
|
}
|
|
243
287
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
288
|
+
try {
|
|
289
|
+
await proxy(upstreamUrl, originalMethod, body, 0)
|
|
290
|
+
} catch (err) {
|
|
291
|
+
if (res.headersSent) {
|
|
292
|
+
res.destroy(err instanceof Error ? err : new Error(String(err)))
|
|
293
|
+
return
|
|
294
|
+
}
|
|
295
|
+
applyFetchProxyCors(res)
|
|
296
|
+
res.writeHead(502, { 'Content-Type': 'text/plain' })
|
|
297
|
+
res.end(`fetch proxy error: ${formatFetchProxyError(upstreamUrl.href, err)}`)
|
|
298
|
+
}
|
|
247
299
|
}
|
|
248
300
|
|
|
249
301
|
export function handleAppApiRequest(req: IncomingMessage, res: ServerResponse): boolean {
|