Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
f0226b4
fix(kiro): avoid mixed-script estimator allocations
luvs01 Sep 20, 2026
d9eb5d7
test(kiro): register kiro-wire-estimate in test layout
luvs01 Sep 22, 2026
9f128c1
fix(crash-guard): count unparenthesized JS frames as real throw sites
lidge-jun Sep 22, 2026
5446f75
fix(proxy): keep loopback traffic outside inherited SOCKS
luvs01 Sep 21, 2026
688f60d
fix(proxy): pin non-loopback SOCKS ownership and document the ambient…
luvs01 Sep 21, 2026
20a65e8
fix(oauth): preserve hashes in command code callback JSON
luvs01 Sep 21, 2026
60cbb55
test(oauth): drive Command Code callback JSON through the shared subm…
lidge-jun Sep 22, 2026
9567ead
docs(remote-hub): warn about shared-host unauthenticated loopback com…
luvs01 Sep 21, 2026
47a8128
docs(remote-hub): align the loopback warning with the reference and c…
luvs01 Sep 21, 2026
390f12c
fix(oauth): drop legacy credential backup on destructive mutations
luvs01 Sep 17, 2026
b05a544
docs(structure): note legacy backup removal on destructive auth mutat…
luvs01 Sep 17, 2026
b8242f8
fix(oauth): only drop the downgrade backup when a credential was actu…
lidge-jun Sep 18, 2026
2853a8a
fix(oauth): drop the downgrade backup when a provider is deleted
lidge-jun Sep 22, 2026
aba89d0
fix(crash-guard): log a new hidden throw site inside the benign fold …
lidge-jun Sep 22, 2026
fa49bea
fix(proxy): merge loopback into an inherited lowercase no_proxy
lidge-jun Sep 22, 2026
c0319d5
fix(oauth): keep other providers in the downgrade backup on deletion
lidge-jun Sep 22, 2026
c623e44
fix(oauth): check an explicit #state on a raw Command Code paste
lidge-jun Sep 22, 2026
373d67b
test: tighten the carried Kiro estimate and remote-hub warning checks
lidge-jun Sep 22, 2026
c86c1a3
fix(crash-guard): keep the hidden throw-site read inside the handler
lidge-jun Sep 22, 2026
8c5f4b1
fix(oauth): never write the downgrade backup through a link or claim it
lidge-jun Sep 22, 2026
d3c3cb9
test(kiro): make the pure-CJK estimate case actually zero-Latin
lidge-jun Sep 22, 2026
748e7a8
fix(oauth): rewrite the downgrade backup through the shared writer
lidge-jun Sep 22, 2026
48215a1
docs(structure): state when an existing OAuth downgrade copy is rewri…
lidge-jun Sep 22, 2026
f6e65bf
fix(proxy): scope the inherited-proxy loopback bypass to exact hosts
lidge-jun Sep 22, 2026
51585db
fix(proxy): key the loopback bypass on SOCKS precedence
lidge-jun Sep 22, 2026
70384c7
fix(proxy): keep 127.0.0.1 off an inherited HTTP proxy without suffix…
lidge-jun Sep 22, 2026
e62eebd
fix(proxy): add bare localhost only when SOCKS is the sole inherited …
lidge-jun Sep 22, 2026
5607c7d
fix(proxy): never add a bare localhost to an inherited lowercase no_p…
lidge-jun Sep 22, 2026
00e5111
fix(proxy): add only loopback addresses to an inherited lowercase no_…
lidge-jun Sep 22, 2026
1d44ed5
test(oauth): assert a cleared provider set as null, as getAccountSet …
lidge-jun Sep 22, 2026
192ad1c
Merge remote-tracking branch 'origin/dev' into codex/260923-luvs-l3-s…
lidge-jun Sep 22, 2026
7cabef8
fix: bypass inherited HTTP ALL_PROXY for loopback
lidge-jun Sep 23, 2026
4e13ee5
fix: keep localhost direct with mixed inherited proxies
lidge-jun Sep 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions docs-site/src/content/docs/guides/remote-hub.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,18 @@ Bind the data listener to the hub's Tailscale address, enable the loopback compa
own processes reach that same port without a credential, and publish management separately. The
values below are examples:

:::danger[Use a dedicated single-tenant host]
Comment thread
coderabbitai[bot] marked this conversation as resolved.
The loopback companion is unauthenticated: every process and OS user on this machine can use the
hub's provider credentials and account quota, and can exhaust the shared turn capacity that
authenticated remote clients depend on. Do not enable it on a shared or multi-tenant host. If the
host is shared, leave the `unauthenticatedLoopbackListener` setting disabled and do not run the hub's local
integrations.

Binding to `127.0.0.1` means the kernel refuses remote connections, but it does not stop a browser:
a page you visit can make your browser connect to `127.0.0.1`. The listener therefore applies the
same `Host` and `Origin` checks as an ordinary loopback bind.
:::

```bash
ocx config set runtimeRole hub
ocx config set hostname 100.64.0.10
Expand Down Expand Up @@ -216,6 +228,9 @@ separate ports:
ocx config set unauthenticatedLoopbackListener '{"enabled":true,"port":10104}'
```

The ported form is the same unauthenticated surface: the dedicated-host warning above applies to
this command too.

With a `port` set, the local integrations follow the listener and write `http://127.0.0.1:10104`
instead. The port must differ from the proxy port and is never OS-assigned: an ephemeral port would
change across restarts while already-running app-servers kept the previous `base_url`.
Expand Down
8 changes: 8 additions & 0 deletions docs-site/src/content/docs/ko/guides/remote-hub.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ ocx sync

데이터 리스너는 허브의 Tailscale 주소에 바인드하고, 허브 자신의 프로세스가 같은 포트를 자격 증명 없이 쓸 수 있도록 루프백 companion을 켜고, 관리 평면은 따로 공개합니다. 아래 값은 예시입니다.

:::danger[전용 단일 테넌트 호스트를 사용하세요]
루프백 companion은 인증이 없습니다. 이 머신의 모든 프로세스와 OS 사용자가 허브의 프로바이더 자격 증명과 계정 쿼터를 사용할 수 있고, 인증된 원격 클라이언트가 의존하는 공유 턴 용량을 고갈시킬 수 있습니다. 공유 또는 다중 테넌트 호스트에서는 활성화하지 마세요. 호스트가 공유라면 `unauthenticatedLoopbackListener` 설정을 비활성화하고 허브의 로컬 통합을 실행하지 마세요.

`127.0.0.1`에 바인드하면 커널이 원격 접속을 거부하지만 브라우저까지 막지는 못합니다. 방문한 페이지가 브라우저를 통해 `127.0.0.1`에 접속하게 할 수 있습니다. 그래서 리스너는 일반 루프백 바인드와 같은 `Host`/`Origin` 검사를 적용합니다.
:::

```bash
ocx config set runtimeRole hub
ocx config set hostname 100.64.0.10
Expand Down Expand Up @@ -115,6 +121,8 @@ companion 형태는 `hostname`이 루프백도 와일드카드도 아닌 구체
ocx config set unauthenticatedLoopbackListener '{"enabled":true,"port":10104}'
```

포트 지정 형태도 같은 인증 없는 표면입니다. 위의 전용 호스트 경고가 이 명령에도 적용됩니다.

`port`를 지정하면 로컬 통합이 리스너를 따라 `http://127.0.0.1:10104`를 기록합니다. 이 포트는 프록시 포트와 달라야 하고 OS가 자동 할당하지 않습니다. 임시 포트는 재시작 때마다 바뀌는데 이미 실행 중인 app-server는 예전 `base_url`을 들고 있기 때문입니다.

**이 필드를 바꾸면 프록시를 재시작하세요.** 소켓은 시작할 때 한 번 바인드되고 로컬 클라이언트 파일도 그때 결정된 값으로 기록되므로, 실행 중인 허브는 예전 답을 유지합니다. 포트 지정 허브에서는 이것이 `ocx claude`가 리스너에 닿는지 `404`를 받는지의 차이입니다. 백그라운드 서비스라면 명령은 항상 재시작하는 `ocx service restart`입니다. [macOS 서비스 운영](#macos-서비스-운영)을 보세요. `ocx restart`는 다른 명령입니다. 직접 띄운 프록시 프로세스를 재시작하며, 서비스 관리자가 감독하는 서비스를 다루지 않습니다.
Expand Down
1 change: 1 addition & 0 deletions scripts/test-layout/layout.json
Original file line number Diff line number Diff line change
Expand Up @@ -895,6 +895,7 @@
"kiro-usage-quota.test.ts": "providers/kiro",
"kiro-windows-cli-db-path.test.ts": "providers/kiro",
"kiro-windows-cli-executable-path.test.ts": "providers/kiro",
"kiro-wire-estimate.test.ts": "providers/kiro",
"lab-activation.test.ts": "lab",
"lab-automation-coderabbit-regressions.test.ts": "lab",
"lab-automation-final-coderabbit-regressions.test.ts": "lab",
Expand Down
5 changes: 5 additions & 0 deletions skills/ocx/references/05_remote_hub.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ has to learn a new port. Setting a `port` (`{ "enabled": true, "port": 10104 }`)
works and puts the two surfaces on separate ports; local integrations then follow the
listener's port.

This listener is unauthenticated: every process and OS user on the hub machine can spend
its provider credentials and quota, and can exhaust the shared turn capacity remote
clients depend on. Enable it only on a dedicated single-tenant host — on a shared or
multi-tenant host, omit `unauthenticatedLoopbackListener` entirely.

The port-less form is refused on a loopback or wildcard `hostname``127.0.0.1`,
`localhost`, `0.0.0.0`, `::` — because the public listener already holds that loopback
address. The refusal happens at write time and again at startup, naming the collision. On
Expand Down
7 changes: 4 additions & 3 deletions src/adapters/kiro/usage.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { estimateTokens } from "../../lib/token-estimate";
import { estimateTokens, estimateTokensFromCharacterCounts } from "../../lib/token-estimate";
import { KIRO_MODEL_CONTEXT_WINDOWS, normalizeKiroModelId } from "../../providers/kiro-models";
import { modelRecordValue } from "../../reasoning-effort";
import { sniffImageDimensions } from "../anthropic-image-guard";
Expand Down Expand Up @@ -98,8 +98,9 @@ export function estimateKiroWireTokens(text: string, modelId: string): number {
if (!text) return 0;
const cjk = kiroCjkCount(text);
if (cjk === 0) return Math.ceil(estimateKiroTokens(text, modelId) * KIRO_LATIN_WIRE_EXPANSION);
const latinTokens = estimateKiroTokens("x".repeat(text.length - cjk), modelId);
const cjkTokens = estimateKiroTokens("\uac00".repeat(cjk), modelId);
const prefixedModelId = `kiro/${modelId}`;
const latinTokens = estimateTokensFromCharacterCounts(text.length - cjk, 0, prefixedModelId);
const cjkTokens = estimateTokensFromCharacterCounts(0, cjk, prefixedModelId);
return Math.ceil(latinTokens * KIRO_LATIN_WIRE_EXPANSION + cjkTokens);
}

Expand Down
13 changes: 12 additions & 1 deletion src/config/atomic-write.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,8 +277,9 @@ function atomicWriteFileToTarget(
target: string,
io?: AtomicWriteIO,
hooks: AtomicWriteHooks = {},
recordOwnership = true,
): void {
recordOwnedConfigPath(getConfigDir(), path);
if (recordOwnership) recordOwnedConfigPath(getConfigDir(), path);
assertResolvedTargetAllowed(path, target);
const tmp = `${target}.ocx.${process.pid}.${nextAtomicTempSequence()}.tmp`;
let hardened = false;
Expand Down Expand Up @@ -399,6 +400,16 @@ export function atomicWriteFileNoFollow(
atomicWriteFileToTarget(path, content, join(resolveWriteTarget(dirname(path)), basename(path)), io, hooks);
}

/**
* The no-follow replacement above, for rewriting a file whose uninstall ownership must stay as
* it was: it does not record the path in the owner manifest. Used to rewrite the OAuth downgrade
* backup, which a pre-registration install may have left deliberately unclaimed; claiming it
* here would let a later uninstall delete recovery data it never owned.
*/
export function atomicWriteFileNoFollowUnclaimed(path: string, content: string): void {
atomicWriteFileToTarget(path, content, join(resolveWriteTarget(dirname(path)), basename(path)), undefined, {}, false);
}

export interface AtomicWriteAsyncIO {
write: (path: string, content: string) => void | Promise<void>;
harden: (path: string) => void | Promise<void>;
Expand Down
74 changes: 62 additions & 12 deletions src/config/proxy-env.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { configureSocks5Fetch } from "../lib/proxy-env";
import { configureSocks5Fetch, socks5ProxyFromEnv } from "../lib/proxy-env";
import { redactUrlForLog } from "../lib/redact";
import { join } from "node:path";
import { DEFAULT_SUBAGENT_MODELS, SUBAGENT_MODELS_VERSION } from "./subagent-models";
Expand Down Expand Up @@ -98,6 +98,60 @@ function warnProxyConfigDiscardOnce(kind: "proxy" | "noProxy" | "noProxyElements
}
}

const LOOPBACK_NO_PROXY = ["localhost", "127.0.0.1", "::1", "[::1]"] as const;
const LOOPBACK_ADDRESS_NO_PROXY = ["127.0.0.1", "::1", "[::1]"] as const;

// With no config.proxy, which loopback bypasses are written depends on who reads them. The
// installed SOCKS fetch wrapper (src/lib/proxy-env.ts configuredOutboundFetch) matches these
// entries as exact hosts. Bun applies an inherited HTTP(S) proxy itself and matches NO_PROXY
// entries as domain suffixes, so a bare "localhost" there would also send any *.localhost name
// direct, including from a fetch that never passes the wrapper. The full list is therefore
// written only when an inherited SOCKS proxy is the only one. Whenever Bun applies an inherited
// HTTP(S) proxy, only the loopback addresses are added: they cannot widen that way (a URL host
// ending in a numeric label parses as IPv4) and keep local health and management calls to
// 127.0.0.1 off the proxy. A proxy-free process is left untouched: writing NO_PROXY into it is
// itself a proxy-env mutation callers observe (the lab sandbox rejects these keys).
function inheritedLoopbackBypass(): readonly string[] | undefined {
const schemeProxy = ["HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy"].some(key => process.env[key]?.trim());
Comment thread
lidge-jun marked this conversation as resolved.
const httpAllProxy = ["ALL_PROXY", "all_proxy"].some(key => {
const value = process.env[key]?.trim();
if (!value) return false;
try {
return ["http:", "https:"].includes(new URL(value).protocol);
} catch {
return false;
}
});
if (schemeProxy || httpAllProxy) return LOOPBACK_ADDRESS_NO_PROXY;
return socks5ProxyFromEnv() !== undefined ? LOOPBACK_NO_PROXY : undefined;
}

function withNoProxyEntries(existing: string, configured: readonly string[], loopback: readonly string[]): string {
const entries = existing.split(",").map(s => s.trim()).filter(Boolean);
const seen = new Set(entries.map(entry => entry.toLowerCase()));
for (const host of [...configured, ...loopback]) {
const key = host.toLowerCase();
if (!seen.has(key)) {
entries.push(host);
seen.add(key);
}
}
return entries.join(",");
}

function mergeNoProxyEntries(configured: readonly string[] = [], loopback: readonly string[] = LOOPBACK_NO_PROXY): void {
process.env.NO_PROXY = withNoProxyEntries(process.env.NO_PROXY ?? process.env.no_proxy ?? "", configured, loopback);
// Bun's native fetch reads a non-empty lowercase no_proxy before NO_PROXY
// (src/codex/catalog/remote.ts), so an inherited one would shadow the loopback entries above.
// Only the loopback addresses join it: Bun matches entries as domain suffixes, and any name
// (a bare "localhost", or a configured noProxy entry the inherited value always shadowed)
// would send its subdomains past a proxy the inherited value kept them on.
const inherited = process.env.no_proxy;
if (inherited !== undefined && inherited.trim() !== "") {
process.env.no_proxy = withNoProxyEntries(inherited, [], loopback.filter(host => host !== "localhost"));
}
}

/**
* Mirror `config.proxy` into HTTP(S)_PROXY env vars. Bun fetch consumes them natively; transports
* such as the ChatGPT upstream WebSocket select the same environment explicitly. User-set HTTP(S)_PROXY
Expand Down Expand Up @@ -130,6 +184,12 @@ export function applyProxyEnvWith(
let proxy = typeof rawProxy === "string" ? resolveEnvValue(rawProxy) : undefined;
if (!proxy) {
if (rawProxy !== undefined) warnProxyConfigDiscardOnce("proxy");
// Inherited-SOCKS path: only loopback bypasses are appended. A configured noProxy is
// deliberately NOT merged here — with no config.proxy the operator's bypass list has
// no declared proxy to apply against, and merging it would silently widen direct
// egress beyond the loopback fix this branch exists for.
const loopback = inheritedLoopbackBypass();
if (loopback) mergeNoProxyEntries([], loopback);
configureSocks5Fetch();
return;
}
Expand Down Expand Up @@ -178,9 +238,6 @@ export function applyProxyEnvWith(
if (!process.env.HTTPS_PROXY?.trim() && !process.env.https_proxy?.trim()) process.env.HTTPS_PROXY = proxy;
}
}
const existing = process.env.NO_PROXY ?? process.env.no_proxy ?? "";
const entries = existing.split(",").map(s => s.trim()).filter(Boolean);
const seen = new Set(entries.map(e => e.toLowerCase()));
// Configured entries first, then loopback: loopback is unconditional, so appending it last
// keeps it present even when the operator lists a loopback host themselves.
const raw = config.noProxy;
Expand All @@ -200,13 +257,6 @@ export function applyProxyEnvWith(
const configured = configuredEntries
.map(entry => entry.trim())
.filter(Boolean);
for (const host of [...configured, "localhost", "127.0.0.1", "::1", "[::1]"]) {
const key = host.toLowerCase();
if (!seen.has(key)) {
entries.push(host);
seen.add(key);
}
}
process.env.NO_PROXY = entries.join(",");
mergeNoProxyEntries(configured);
configureSocks5Fetch();
}
56 changes: 52 additions & 4 deletions src/lib/crash-guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,9 @@ function safeStringify(value: unknown): string {

let benignSuppressed = 0;
let benignLastLoggedAt = 0;
let benignLastOrigin: string | undefined;
const BENIGN_LOG_INTERVAL_MS = 5 * 60_000;
const MAX_BENIGN_ORIGIN_BYTES = 1024;

/**
* Bun raises an off-path `unhandledRejection: TypeError: null is not an object` (native-only stack)
Expand All @@ -176,17 +178,53 @@ export function isBenignAbortTeardown(err: unknown): boolean {
const lockedStreamTeardown = err.message === "Invalid state: ReadableStream is locked"
&& (err as { code?: unknown }).code === "ERR_INVALID_STATE";
if (!bareNullTeardown && !lockedStreamTeardown) return false;
const stack = err.stack ?? "";
// Native-only: no JS source frame. A real app TypeError would carry a `(file:line:col)` frame.
return !/\((?!native:)[^)]*:\d+:\d+\)/.test(stack);
// Native-only: no JS source frame, parenthesized or not. Hidden JSC source fields
// (sourceURL/line/column) do not decide this: Bun can attach them to errors raised from
// its own builtin frames, and the benign summary still records them through diagnose().
return !hasJsSourceFrame(err.stack ?? "");
}

/**
* True when a stack line is an `at …` frame ending in `line:col` (optionally inside
* parentheses) whose location is not a Bun builtin (`native:`). Covers `at fn (/abs/x.ts:1:2)`,
* `at /abs/x.ts:1:2`, `at async fn (file:///x.ts:1:2)` and Windows drive paths.
*/
function hasJsSourceFrame(stack: string): boolean {
return stack.split(/\r?\n/).some(raw => {
const frame = raw.trim();
return frame.startsWith("at ")
&& /:\d+:\d+\)?$/.test(frame)
&& !/[(\s]native:\d+:\d+\)?$/.test(frame);
});
}

/**
* The JSC hidden throw site (`sourceURL:line:col`), when the error carries one. Best-effort:
* this runs inside the process crash handler, so an accessor that throws yields no site.
*/
function hiddenThrowSite(err: unknown): string | undefined {
try {
if (!err || typeof err !== "object") return undefined;
const e = err as Record<string, unknown>;
if (typeof e.sourceURL !== "string" || !e.sourceURL) return undefined;
const site = `${e.sourceURL}:${String(e.line ?? e.originalLine ?? "")}:${String(e.column ?? e.originalColumn ?? "")}`;
return truncateRetainedUtf8(site, MAX_BENIGN_ORIGIN_BYTES);
} catch {
return undefined;
}
}

function record(kind: string, err: unknown, promise?: unknown): void {
if (kind === "unhandledRejection" && isBenignAbortTeardown(err)) {
benignSuppressed++;
const now = Date.now();
if (now - benignLastLoggedAt < BENIGN_LOG_INTERVAL_MS) return; // fold repeats silently
// A throw site JSC recorded on hidden fields is new information when it differs from
// the last one logged, so it is written even inside the fold window; repeats still fold.
const origin = hiddenThrowSite(err);
const novelOrigin = origin !== undefined && origin !== benignLastOrigin;
if (!novelOrigin && now - benignLastLoggedAt < BENIGN_LOG_INTERVAL_MS) return; // fold repeats silently
benignLastLoggedAt = now;
if (origin !== undefined) benignLastOrigin = origin;
const summary = `\n[${new Date(now).toISOString()}] benign-abort-teardown x${benignSuppressed}`
+ ` (Bun fetch-body abort; proxy unaffected)${diagnose(err)}${diagnosePromise(promise)}${breadcrumb()}\n`;
benignSuppressed = 0;
Expand Down Expand Up @@ -306,6 +344,16 @@ export function resetCrashRingForTests(): void {
fetchRingBytes = 0;
}

export function recordCrashForTests(kind: string, err: unknown): void {
record(kind, err);
}

export function resetBenignFoldForTests(): void {
benignSuppressed = 0;
benignLastLoggedAt = 0;
benignLastOrigin = undefined;
}

/** Render the recent fetch ring (pending first) for the crash breadcrumb. */
function recentFetches(): string {
try {
Expand Down
19 changes: 18 additions & 1 deletion src/lib/proxy-env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export function noProxyMatches(
if (!entry) continue;
if (entry === "*") return true;
entry = entry.replace(/^(?:https?|wss?):\/\//, "").split("/", 1)[0]!;
const domainForm = /^\*?\./.test(entry);

let entryHost = entry;
let entryPort = "";
Expand All @@ -46,11 +47,20 @@ export function noProxyMatches(
}
if (entryPort && entryPort !== port) continue;
entryHost = normalizeProxyHostname(entryHost.replace(/^\*?\./, ""));
if (entryHost && (hostname === entryHost || hostname.endsWith(`.${entryHost}`))) return true;
if (!entryHost) continue;
if (hostname === entryHost) return true;
// A bare loopback name or an IP literal names one host: "localhost" must not send
// "anything.localhost" direct, which need not resolve to loopback. ".localhost" still does.
if (!domainForm && isExactOnlyNoProxyHost(entryHost)) continue;
if (hostname.endsWith(`.${entryHost}`)) return true;
}
return false;
}

function isExactOnlyNoProxyHost(host: string): boolean {
return host === "localhost" || host.includes(":") || /^\d{1,3}(?:\.\d{1,3}){3}$/.test(host);
}

export function resolveProxyRoute(
url: URL,
env: ProxyEnvMap = process.env,
Expand Down Expand Up @@ -211,6 +221,13 @@ export function configuredOutboundFetch(
} catch {
return base!(input, init);
}
// A mixed inherited SOCKS/HTTP ALL_PROXY environment cannot put bare "localhost" in
// NO_PROXY: Bun would also bypass its HTTP proxy for app.localhost. Keep the name exact
// here and force native fetch direct so the opposite-case HTTP proxy cannot take over.
if (proxy && explicitProxy === undefined && (url.protocol === "http:" || url.protocol === "https:")
&& normalizeProxyHostname(url.hostname) === "localhost") {
return base!(input, { ...init, proxy: false } as ProxyCapableRequestInit);
}
if (proxy && (url.protocol === "http:" || url.protocol === "https:") && (explicitProxy !== undefined || !noProxyMatches(url))) {
return socks5Fetch(input, init, proxy);
}
Expand Down
Loading
Loading