@introspection-sdk/introspection-proxy 0.1.0 → 0.4.7
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/.turbo/turbo-build.log +1 -1
- package/README.md +45 -0
- package/dist/proxy.d.ts +4 -39
- package/dist/proxy.d.ts.map +1 -1
- package/dist/proxy.js +59 -86
- package/dist/proxy.js.map +1 -1
- package/package.json +1 -1
- package/src/proxy.ts +72 -110
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
|
|
2
|
-
> @introspection-sdk/introspection-proxy@0.
|
|
2
|
+
> @introspection-sdk/introspection-proxy@0.4.7 build /home/runner/work/introspection-js-sdk/introspection-js-sdk/packages/introspection-proxy
|
|
3
3
|
> tsc
|
|
4
4
|
|
package/README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# @introspection-sdk/introspection-proxy
|
|
2
|
+
|
|
3
|
+
Route outbound `fetch` calls through the Introspection egress proxy for credential injection, or through a standard CONNECT forward proxy.
|
|
4
|
+
|
|
5
|
+
## Quick start
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { installProxyFetch } from "@introspection-sdk/introspection-proxy";
|
|
9
|
+
|
|
10
|
+
// Replace globalThis.fetch — all subsequent fetch() calls route through the proxy.
|
|
11
|
+
// No-op when no proxy env vars are set (safe for local dev).
|
|
12
|
+
installProxyFetch();
|
|
13
|
+
|
|
14
|
+
// Every fetch now goes through the configured proxy.
|
|
15
|
+
const res = await fetch("https://api.openai.com/v1/models");
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Environment variables
|
|
19
|
+
|
|
20
|
+
| Variable | Purpose |
|
|
21
|
+
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
22
|
+
| `INTROSPECTION_EGRESS_URL` | Egress reverse proxy URL (e.g. `http://localhost:10000`). Sends requests as plain HTTP so the proxy can route by `Host` header and inject credentials via ext_proc. |
|
|
23
|
+
| `INTROSPECTION_ENDPOINT_HOSTS` | Comma-separated hostnames that should use the egress proxy (e.g. `api.openai.com,api.anthropic.com`). When set, only matching hosts use egress; all others fall through to the CONNECT proxy. When unset, all traffic goes through egress. |
|
|
24
|
+
| `HTTPS_PROXY` / `HTTP_PROXY` | Standard forward proxy. Used as the CONNECT tunnel for hosts not in `INTROSPECTION_ENDPOINT_HOSTS`. |
|
|
25
|
+
|
|
26
|
+
## Per-client usage
|
|
27
|
+
|
|
28
|
+
If you only want to proxy a single client (not the whole process):
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { createProxyFetch } from "@introspection-sdk/introspection-proxy";
|
|
32
|
+
import { createClient } from "@supabase/supabase-js";
|
|
33
|
+
|
|
34
|
+
const supabase = createClient(url, key, {
|
|
35
|
+
global: { fetch: createProxyFetch() },
|
|
36
|
+
});
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## How it works
|
|
40
|
+
|
|
41
|
+
Two proxy modes, selected per-request:
|
|
42
|
+
|
|
43
|
+
- **Egress credential-injection proxy** (`INTROSPECTION_EGRESS_URL`): plain-HTTP reverse proxy that routes by `Host` header and injects upstream credentials via ext_proc. Only used for hosts in `INTROSPECTION_ENDPOINT_HOSTS`.
|
|
44
|
+
|
|
45
|
+
- **Forward CONNECT proxy** (`HTTPS_PROXY`): opaque TLS tunnel the proxy cannot read or modify. Used for all other hosts (S3, GitHub, npm, etc.).
|
package/dist/proxy.d.ts
CHANGED
|
@@ -1,51 +1,16 @@
|
|
|
1
1
|
import { type Dispatcher } from "undici";
|
|
2
2
|
export interface ProxyFetchOptions {
|
|
3
|
-
/**
|
|
4
|
-
* Egress (credential-injection) proxy URL. Defaults to
|
|
5
|
-
* `process.env.EGRESS_PROXY_URL` (e.g. `http://localhost:10000`).
|
|
6
|
-
*/
|
|
7
3
|
egressProxyUrl?: string;
|
|
8
|
-
|
|
9
|
-
* Standard forward-proxy URL. Defaults to the `HTTPS_PROXY` / `HTTP_PROXY`
|
|
10
|
-
* environment variables (which also honour `NO_PROXY`). Used only when no
|
|
11
|
-
* egress proxy is configured.
|
|
12
|
-
*
|
|
13
|
-
* Note: this is a plain forward proxy (a `CONNECT` tunnel for `https:`
|
|
14
|
-
* targets) and does **not** inject credentials — it only routes traffic. For
|
|
15
|
-
* the Introspection egress proxy that injects credentials, use
|
|
16
|
-
* {@link ProxyFetchOptions.egressProxyUrl} / `EGRESS_PROXY_URL` instead.
|
|
17
|
-
*/
|
|
4
|
+
egressProxyHosts?: string;
|
|
18
5
|
forwardProxyUrl?: string;
|
|
19
6
|
}
|
|
20
|
-
/**
|
|
21
|
-
* Resolve a standard forward-proxy URL from the environment, honouring the
|
|
22
|
-
* conventional `HTTPS_PROXY` / `HTTP_PROXY` variables (and their lowercase
|
|
23
|
-
* forms). Shared with the OTLP exporter proxy helper (`withOtlpHttpsProxy`) so
|
|
24
|
-
* fetch traffic and OTLP traffic resolve the same proxy.
|
|
25
|
-
*/
|
|
26
7
|
export declare function resolveForwardProxyUrl(): string | undefined;
|
|
27
8
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
9
|
+
* Returns a single dispatcher. When egress hosts are scoped, returns the
|
|
10
|
+
* forward dispatcher (the egress/forward split is per-request inside
|
|
11
|
+
* {@link createProxyFetch}).
|
|
31
12
|
*/
|
|
32
13
|
export declare function getProxyDispatcher(options?: ProxyFetchOptions): Dispatcher | undefined;
|
|
33
|
-
/**
|
|
34
|
-
* A `fetch`-compatible function that routes through the configured proxy.
|
|
35
|
-
* Drop-in for any client that accepts a `fetch` (e.g. supabase-js
|
|
36
|
-
* `createClient(url, key, { global: { fetch: createProxyFetch() } })`).
|
|
37
|
-
*
|
|
38
|
-
* When no proxy is configured this returns the global `fetch` unchanged, so it
|
|
39
|
-
* is safe to use unconditionally (local dev keeps talking directly to APIs).
|
|
40
|
-
*/
|
|
41
14
|
export declare function createProxyFetch(options?: ProxyFetchOptions): typeof fetch;
|
|
42
|
-
/**
|
|
43
|
-
* Replace `globalThis.fetch` so existing bare `fetch(...)` calls — and any SDK
|
|
44
|
-
* that uses the global fetch by default, including supabase-js — route through
|
|
45
|
-
* the configured proxy with no other code changes. No-op when no proxy is
|
|
46
|
-
* configured.
|
|
47
|
-
*
|
|
48
|
-
* Returns a function that restores the original `fetch`.
|
|
49
|
-
*/
|
|
50
15
|
export declare function installProxyFetch(options?: ProxyFetchOptions): () => void;
|
|
51
16
|
//# sourceMappingURL=proxy.d.ts.map
|
package/dist/proxy.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"AAiBA,OAAO,EAKL,KAAK,UAAU,EAChB,MAAM,QAAQ,CAAC;AAEhB,MAAM,WAAW,iBAAiB;IAChC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAiBD,wBAAgB,sBAAsB,IAAI,MAAM,GAAG,SAAS,CAQ3D;AA0BD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,GAAE,iBAAsB,GAC9B,UAAU,GAAG,SAAS,CASxB;AAaD,wBAAgB,gBAAgB,CAC9B,OAAO,GAAE,iBAAsB,GAC9B,OAAO,KAAK,CAoDd;AAED,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,iBAAsB,GAAG,MAAM,IAAI,CAQ7E"}
|
package/dist/proxy.js
CHANGED
|
@@ -1,58 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Outbound HTTP proxy helpers for `fetch`-based clients.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* custom `fetch` (e.g. `@supabase/supabase-js` via `global.fetch`), or every
|
|
6
|
-
* request in a process (via {@link installProxyFetch}) — be routed through a
|
|
7
|
-
* proxy so the caller does not have to hold upstream credentials.
|
|
4
|
+
* Two proxy modes, selected per-request:
|
|
8
5
|
*
|
|
9
|
-
*
|
|
6
|
+
* - **Egress credential-injection proxy** (`INTROSPECTION_EGRESS_URL`):
|
|
7
|
+
* plain-HTTP reverse proxy that routes by `Host` header and injects upstream
|
|
8
|
+
* credentials via ext_proc. Only used for hosts listed in
|
|
9
|
+
* `INTROSPECTION_ENDPOINT_HOSTS` (comma-separated). When the host list is
|
|
10
|
+
* empty, all traffic goes through egress (legacy behaviour).
|
|
10
11
|
*
|
|
11
|
-
* - **
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* credential (so the process can send a placeholder/locator token). We dial
|
|
15
|
-
* the proxy on every connection but leave the request — including its real
|
|
16
|
-
* `Host` and `https:` scheme — untouched, which is exactly what the proxy
|
|
17
|
-
* needs to route and inject. undici speaks HTTP/1.1 over the socket we hand
|
|
18
|
-
* it, so no URL rewriting is required.
|
|
19
|
-
*
|
|
20
|
-
* - **Standard forward (CONNECT) proxy** (`HTTPS_PROXY` / `HTTP_PROXY`,
|
|
21
|
-
* honouring `NO_PROXY`): the same kind of proxy the OTLP exporter uses, but
|
|
22
|
-
* exposed as an undici dispatcher so it works with `fetch`.
|
|
23
|
-
* (`HttpsProxyAgent` is a Node `http.Agent` and is ignored by `fetch`, which
|
|
24
|
-
* only accepts an undici `dispatcher`.) This is plain forward-proxying and
|
|
25
|
-
* does **not** inject credentials: for `https:` targets it uses HTTP
|
|
26
|
-
* `CONNECT`, so the connection is an opaque end-to-end TLS tunnel the proxy
|
|
27
|
-
* cannot read or modify. Credential injection happens **only** in egress
|
|
28
|
-
* mode above (where the request is sent to the proxy in plaintext so it can
|
|
29
|
-
* route by `Host` and inject). Use this branch for consumers that sit behind
|
|
30
|
-
* a generic corporate/forward proxy.
|
|
31
|
-
*
|
|
32
|
-
* `EGRESS_PROXY_URL` takes precedence when both are set.
|
|
33
|
-
*
|
|
34
|
-
* Note: in egress mode the dispatcher routes *all* requests through the proxy,
|
|
35
|
-
* so destinations must be configured on the proxy. This matches the intended
|
|
36
|
-
* "all egress goes through the proxy" model for sandboxed recipes.
|
|
12
|
+
* - **Forward CONNECT proxy** (`HTTPS_PROXY` / `HTTP_PROXY`): opaque TLS
|
|
13
|
+
* tunnel the proxy cannot read or modify. Used for all other hosts (S3,
|
|
14
|
+
* GitHub, npm, etc.).
|
|
37
15
|
*/
|
|
38
16
|
import { Socket } from "node:net";
|
|
39
17
|
import { connect as tlsConnect } from "node:tls";
|
|
40
|
-
import { Agent, EnvHttpProxyAgent,
|
|
41
|
-
function
|
|
42
|
-
return options.egressProxyUrl ?? process.env.
|
|
18
|
+
import { Agent, EnvHttpProxyAgent, fetch as undiciFetch, } from "undici";
|
|
19
|
+
function resolveEgressUrl(options) {
|
|
20
|
+
return options.egressProxyUrl ?? process.env.INTROSPECTION_EGRESS_URL;
|
|
21
|
+
}
|
|
22
|
+
function resolveEgressHosts(options) {
|
|
23
|
+
const raw = options.egressProxyHosts ?? process.env.INTROSPECTION_ENDPOINT_HOSTS ?? "";
|
|
24
|
+
return new Set(raw
|
|
25
|
+
.split(",")
|
|
26
|
+
.map((h) => h.trim().toLowerCase())
|
|
27
|
+
.filter(Boolean));
|
|
43
28
|
}
|
|
44
|
-
/**
|
|
45
|
-
* Resolve a standard forward-proxy URL from the environment, honouring the
|
|
46
|
-
* conventional `HTTPS_PROXY` / `HTTP_PROXY` variables (and their lowercase
|
|
47
|
-
* forms). Shared with the OTLP exporter proxy helper (`withOtlpHttpsProxy`) so
|
|
48
|
-
* fetch traffic and OTLP traffic resolve the same proxy.
|
|
49
|
-
*/
|
|
50
29
|
export function resolveForwardProxyUrl() {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
env.
|
|
54
|
-
env.
|
|
55
|
-
env.http_proxy ||
|
|
30
|
+
return (process.env.HTTPS_PROXY ||
|
|
31
|
+
process.env.https_proxy ||
|
|
32
|
+
process.env.HTTP_PROXY ||
|
|
33
|
+
process.env.http_proxy ||
|
|
56
34
|
undefined);
|
|
57
35
|
}
|
|
58
36
|
function buildEgressDispatcher(proxyUrl) {
|
|
@@ -60,8 +38,6 @@ function buildEgressDispatcher(proxyUrl) {
|
|
|
60
38
|
const useTls = url.protocol === "https:";
|
|
61
39
|
const port = Number(url.port) || (useTls ? 443 : 80);
|
|
62
40
|
const host = url.hostname;
|
|
63
|
-
// Always connect to the proxy, regardless of the request's destination. The
|
|
64
|
-
// request keeps its real `Host`, which is what the proxy routes + injects on.
|
|
65
41
|
return new Agent({
|
|
66
42
|
connect(_options, callback) {
|
|
67
43
|
if (useTls) {
|
|
@@ -78,42 +54,47 @@ function buildEgressDispatcher(proxyUrl) {
|
|
|
78
54
|
});
|
|
79
55
|
}
|
|
80
56
|
/**
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
57
|
+
* Returns a single dispatcher. When egress hosts are scoped, returns the
|
|
58
|
+
* forward dispatcher (the egress/forward split is per-request inside
|
|
59
|
+
* {@link createProxyFetch}).
|
|
84
60
|
*/
|
|
85
61
|
export function getProxyDispatcher(options = {}) {
|
|
86
|
-
const egressUrl =
|
|
87
|
-
if (egressUrl) {
|
|
62
|
+
const egressUrl = resolveEgressUrl(options);
|
|
63
|
+
if (egressUrl && resolveEgressHosts(options).size === 0) {
|
|
88
64
|
return buildEgressDispatcher(egressUrl);
|
|
89
65
|
}
|
|
90
|
-
if (options.forwardProxyUrl) {
|
|
91
|
-
return new ProxyAgent(options.forwardProxyUrl);
|
|
92
|
-
}
|
|
93
66
|
if (resolveForwardProxyUrl()) {
|
|
94
|
-
// Reads HTTPS_PROXY / HTTP_PROXY / NO_PROXY from the environment.
|
|
95
67
|
return new EnvHttpProxyAgent();
|
|
96
68
|
}
|
|
97
69
|
return undefined;
|
|
98
70
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
71
|
+
function toUrlString(input) {
|
|
72
|
+
if (typeof input === "string")
|
|
73
|
+
return input;
|
|
74
|
+
if (input instanceof URL)
|
|
75
|
+
return input.toString();
|
|
76
|
+
return input.url;
|
|
77
|
+
}
|
|
78
|
+
function downgradeToHttp(url) {
|
|
79
|
+
const s = typeof url === "string" ? url : url.toString();
|
|
80
|
+
return s.startsWith("https://") ? "http://" + s.slice(8) : s;
|
|
81
|
+
}
|
|
107
82
|
export function createProxyFetch(options = {}) {
|
|
108
|
-
const
|
|
109
|
-
|
|
83
|
+
const egressUrl = resolveEgressUrl(options);
|
|
84
|
+
const egressHosts = resolveEgressHosts(options);
|
|
85
|
+
const egressIsPlainHttp = !!egressUrl && new URL(egressUrl).protocol === "http:";
|
|
86
|
+
const egress = egressUrl ? buildEgressDispatcher(egressUrl) : undefined;
|
|
87
|
+
const forward = resolveForwardProxyUrl()
|
|
88
|
+
? new EnvHttpProxyAgent()
|
|
89
|
+
: undefined;
|
|
90
|
+
if (!egress && !forward)
|
|
110
91
|
return fetch;
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
92
|
+
return (input, init) => {
|
|
93
|
+
const hostname = new URL(toUrlString(input)).hostname.toLowerCase();
|
|
94
|
+
const useEgress = !!egress && (egressHosts.size === 0 || egressHosts.has(hostname));
|
|
95
|
+
const dispatcher = useEgress ? egress : forward;
|
|
96
|
+
if (!dispatcher)
|
|
97
|
+
return fetch(input, init);
|
|
117
98
|
let target;
|
|
118
99
|
let opts;
|
|
119
100
|
if (typeof Request !== "undefined" && input instanceof Request) {
|
|
@@ -128,7 +109,6 @@ export function createProxyFetch(options = {}) {
|
|
|
128
109
|
...init,
|
|
129
110
|
dispatcher,
|
|
130
111
|
};
|
|
131
|
-
// A streamed request body requires half-duplex in undici.
|
|
132
112
|
if (input.body != null)
|
|
133
113
|
opts.duplex = "half";
|
|
134
114
|
}
|
|
@@ -136,25 +116,18 @@ export function createProxyFetch(options = {}) {
|
|
|
136
116
|
target = input;
|
|
137
117
|
opts = { ...init, dispatcher };
|
|
138
118
|
}
|
|
119
|
+
if (useEgress && egressIsPlainHttp) {
|
|
120
|
+
target = downgradeToHttp(target);
|
|
121
|
+
}
|
|
139
122
|
return undiciFetch(target, opts);
|
|
140
123
|
};
|
|
141
|
-
return proxied;
|
|
142
124
|
}
|
|
143
|
-
/**
|
|
144
|
-
* Replace `globalThis.fetch` so existing bare `fetch(...)` calls — and any SDK
|
|
145
|
-
* that uses the global fetch by default, including supabase-js — route through
|
|
146
|
-
* the configured proxy with no other code changes. No-op when no proxy is
|
|
147
|
-
* configured.
|
|
148
|
-
*
|
|
149
|
-
* Returns a function that restores the original `fetch`.
|
|
150
|
-
*/
|
|
151
125
|
export function installProxyFetch(options = {}) {
|
|
152
|
-
const
|
|
153
|
-
if (
|
|
126
|
+
const proxied = createProxyFetch(options);
|
|
127
|
+
if (proxied === fetch)
|
|
154
128
|
return () => { };
|
|
155
|
-
}
|
|
156
129
|
const original = globalThis.fetch;
|
|
157
|
-
globalThis.fetch =
|
|
130
|
+
globalThis.fetch = proxied;
|
|
158
131
|
return () => {
|
|
159
132
|
globalThis.fetch = original;
|
|
160
133
|
};
|
package/dist/proxy.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"proxy.js","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"proxy.js","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@introspection-sdk/introspection-proxy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.7",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "Outbound HTTP proxy helpers for routing fetch-based clients through the Introspection egress proxy or a standard forward proxy",
|
|
6
6
|
"engines": {
|
package/src/proxy.ts
CHANGED
|
@@ -1,87 +1,55 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Outbound HTTP proxy helpers for `fetch`-based clients.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* custom `fetch` (e.g. `@supabase/supabase-js` via `global.fetch`), or every
|
|
6
|
-
* request in a process (via {@link installProxyFetch}) — be routed through a
|
|
7
|
-
* proxy so the caller does not have to hold upstream credentials.
|
|
4
|
+
* Two proxy modes, selected per-request:
|
|
8
5
|
*
|
|
9
|
-
*
|
|
6
|
+
* - **Egress credential-injection proxy** (`INTROSPECTION_EGRESS_URL`):
|
|
7
|
+
* plain-HTTP reverse proxy that routes by `Host` header and injects upstream
|
|
8
|
+
* credentials via ext_proc. Only used for hosts listed in
|
|
9
|
+
* `INTROSPECTION_ENDPOINT_HOSTS` (comma-separated). When the host list is
|
|
10
|
+
* empty, all traffic goes through egress (legacy behaviour).
|
|
10
11
|
*
|
|
11
|
-
* - **
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* credential (so the process can send a placeholder/locator token). We dial
|
|
15
|
-
* the proxy on every connection but leave the request — including its real
|
|
16
|
-
* `Host` and `https:` scheme — untouched, which is exactly what the proxy
|
|
17
|
-
* needs to route and inject. undici speaks HTTP/1.1 over the socket we hand
|
|
18
|
-
* it, so no URL rewriting is required.
|
|
19
|
-
*
|
|
20
|
-
* - **Standard forward (CONNECT) proxy** (`HTTPS_PROXY` / `HTTP_PROXY`,
|
|
21
|
-
* honouring `NO_PROXY`): the same kind of proxy the OTLP exporter uses, but
|
|
22
|
-
* exposed as an undici dispatcher so it works with `fetch`.
|
|
23
|
-
* (`HttpsProxyAgent` is a Node `http.Agent` and is ignored by `fetch`, which
|
|
24
|
-
* only accepts an undici `dispatcher`.) This is plain forward-proxying and
|
|
25
|
-
* does **not** inject credentials: for `https:` targets it uses HTTP
|
|
26
|
-
* `CONNECT`, so the connection is an opaque end-to-end TLS tunnel the proxy
|
|
27
|
-
* cannot read or modify. Credential injection happens **only** in egress
|
|
28
|
-
* mode above (where the request is sent to the proxy in plaintext so it can
|
|
29
|
-
* route by `Host` and inject). Use this branch for consumers that sit behind
|
|
30
|
-
* a generic corporate/forward proxy.
|
|
31
|
-
*
|
|
32
|
-
* `EGRESS_PROXY_URL` takes precedence when both are set.
|
|
33
|
-
*
|
|
34
|
-
* Note: in egress mode the dispatcher routes *all* requests through the proxy,
|
|
35
|
-
* so destinations must be configured on the proxy. This matches the intended
|
|
36
|
-
* "all egress goes through the proxy" model for sandboxed recipes.
|
|
12
|
+
* - **Forward CONNECT proxy** (`HTTPS_PROXY` / `HTTP_PROXY`): opaque TLS
|
|
13
|
+
* tunnel the proxy cannot read or modify. Used for all other hosts (S3,
|
|
14
|
+
* GitHub, npm, etc.).
|
|
37
15
|
*/
|
|
38
16
|
import { Socket } from "node:net";
|
|
39
17
|
import { connect as tlsConnect } from "node:tls";
|
|
40
18
|
import {
|
|
41
19
|
Agent,
|
|
42
20
|
EnvHttpProxyAgent,
|
|
43
|
-
ProxyAgent,
|
|
44
21
|
buildConnector,
|
|
45
22
|
fetch as undiciFetch,
|
|
46
23
|
type Dispatcher,
|
|
47
24
|
} from "undici";
|
|
48
25
|
|
|
49
26
|
export interface ProxyFetchOptions {
|
|
50
|
-
/**
|
|
51
|
-
* Egress (credential-injection) proxy URL. Defaults to
|
|
52
|
-
* `process.env.EGRESS_PROXY_URL` (e.g. `http://localhost:10000`).
|
|
53
|
-
*/
|
|
54
27
|
egressProxyUrl?: string;
|
|
55
|
-
|
|
56
|
-
* Standard forward-proxy URL. Defaults to the `HTTPS_PROXY` / `HTTP_PROXY`
|
|
57
|
-
* environment variables (which also honour `NO_PROXY`). Used only when no
|
|
58
|
-
* egress proxy is configured.
|
|
59
|
-
*
|
|
60
|
-
* Note: this is a plain forward proxy (a `CONNECT` tunnel for `https:`
|
|
61
|
-
* targets) and does **not** inject credentials — it only routes traffic. For
|
|
62
|
-
* the Introspection egress proxy that injects credentials, use
|
|
63
|
-
* {@link ProxyFetchOptions.egressProxyUrl} / `EGRESS_PROXY_URL` instead.
|
|
64
|
-
*/
|
|
28
|
+
egressProxyHosts?: string;
|
|
65
29
|
forwardProxyUrl?: string;
|
|
66
30
|
}
|
|
67
31
|
|
|
68
|
-
function
|
|
69
|
-
return options.egressProxyUrl ?? process.env.
|
|
32
|
+
function resolveEgressUrl(options: ProxyFetchOptions): string | undefined {
|
|
33
|
+
return options.egressProxyUrl ?? process.env.INTROSPECTION_EGRESS_URL;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function resolveEgressHosts(options: ProxyFetchOptions): Set<string> {
|
|
37
|
+
const raw =
|
|
38
|
+
options.egressProxyHosts ?? process.env.INTROSPECTION_ENDPOINT_HOSTS ?? "";
|
|
39
|
+
return new Set(
|
|
40
|
+
raw
|
|
41
|
+
.split(",")
|
|
42
|
+
.map((h) => h.trim().toLowerCase())
|
|
43
|
+
.filter(Boolean),
|
|
44
|
+
);
|
|
70
45
|
}
|
|
71
46
|
|
|
72
|
-
/**
|
|
73
|
-
* Resolve a standard forward-proxy URL from the environment, honouring the
|
|
74
|
-
* conventional `HTTPS_PROXY` / `HTTP_PROXY` variables (and their lowercase
|
|
75
|
-
* forms). Shared with the OTLP exporter proxy helper (`withOtlpHttpsProxy`) so
|
|
76
|
-
* fetch traffic and OTLP traffic resolve the same proxy.
|
|
77
|
-
*/
|
|
78
47
|
export function resolveForwardProxyUrl(): string | undefined {
|
|
79
|
-
const env = process.env;
|
|
80
48
|
return (
|
|
81
|
-
env.HTTPS_PROXY ||
|
|
82
|
-
env.https_proxy ||
|
|
83
|
-
env.HTTP_PROXY ||
|
|
84
|
-
env.http_proxy ||
|
|
49
|
+
process.env.HTTPS_PROXY ||
|
|
50
|
+
process.env.https_proxy ||
|
|
51
|
+
process.env.HTTP_PROXY ||
|
|
52
|
+
process.env.http_proxy ||
|
|
85
53
|
undefined
|
|
86
54
|
);
|
|
87
55
|
}
|
|
@@ -92,8 +60,6 @@ function buildEgressDispatcher(proxyUrl: string): Dispatcher {
|
|
|
92
60
|
const port = Number(url.port) || (useTls ? 443 : 80);
|
|
93
61
|
const host = url.hostname;
|
|
94
62
|
|
|
95
|
-
// Always connect to the proxy, regardless of the request's destination. The
|
|
96
|
-
// request keeps its real `Host`, which is what the proxy routes + injects on.
|
|
97
63
|
return new Agent({
|
|
98
64
|
connect(
|
|
99
65
|
_options: buildConnector.Options,
|
|
@@ -113,56 +79,61 @@ function buildEgressDispatcher(proxyUrl: string): Dispatcher {
|
|
|
113
79
|
}
|
|
114
80
|
|
|
115
81
|
/**
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
82
|
+
* Returns a single dispatcher. When egress hosts are scoped, returns the
|
|
83
|
+
* forward dispatcher (the egress/forward split is per-request inside
|
|
84
|
+
* {@link createProxyFetch}).
|
|
119
85
|
*/
|
|
120
86
|
export function getProxyDispatcher(
|
|
121
87
|
options: ProxyFetchOptions = {},
|
|
122
88
|
): Dispatcher | undefined {
|
|
123
|
-
const egressUrl =
|
|
124
|
-
if (egressUrl) {
|
|
89
|
+
const egressUrl = resolveEgressUrl(options);
|
|
90
|
+
if (egressUrl && resolveEgressHosts(options).size === 0) {
|
|
125
91
|
return buildEgressDispatcher(egressUrl);
|
|
126
92
|
}
|
|
127
|
-
|
|
128
|
-
if (options.forwardProxyUrl) {
|
|
129
|
-
return new ProxyAgent(options.forwardProxyUrl);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
93
|
if (resolveForwardProxyUrl()) {
|
|
133
|
-
// Reads HTTPS_PROXY / HTTP_PROXY / NO_PROXY from the environment.
|
|
134
94
|
return new EnvHttpProxyAgent();
|
|
135
95
|
}
|
|
136
|
-
|
|
137
96
|
return undefined;
|
|
138
97
|
}
|
|
139
98
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
99
|
+
function toUrlString(input: RequestInfo | URL): string {
|
|
100
|
+
if (typeof input === "string") return input;
|
|
101
|
+
if (input instanceof URL) return input.toString();
|
|
102
|
+
return (input as Request).url;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function downgradeToHttp(url: string | URL): string {
|
|
106
|
+
const s = typeof url === "string" ? url : url.toString();
|
|
107
|
+
return s.startsWith("https://") ? "http://" + s.slice(8) : s;
|
|
108
|
+
}
|
|
109
|
+
|
|
148
110
|
export function createProxyFetch(
|
|
149
111
|
options: ProxyFetchOptions = {},
|
|
150
112
|
): typeof fetch {
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
113
|
+
const egressUrl = resolveEgressUrl(options);
|
|
114
|
+
const egressHosts = resolveEgressHosts(options);
|
|
115
|
+
const egressIsPlainHttp =
|
|
116
|
+
!!egressUrl && new URL(egressUrl).protocol === "http:";
|
|
117
|
+
|
|
118
|
+
const egress = egressUrl ? buildEgressDispatcher(egressUrl) : undefined;
|
|
119
|
+
const forward = resolveForwardProxyUrl()
|
|
120
|
+
? new EnvHttpProxyAgent()
|
|
121
|
+
: undefined;
|
|
122
|
+
|
|
123
|
+
if (!egress && !forward) return fetch;
|
|
124
|
+
|
|
125
|
+
return (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
|
|
126
|
+
const hostname = new URL(toUrlString(input)).hostname.toLowerCase();
|
|
127
|
+
|
|
128
|
+
const useEgress =
|
|
129
|
+
!!egress && (egressHosts.size === 0 || egressHosts.has(hostname));
|
|
130
|
+
const dispatcher = useEgress ? egress : forward;
|
|
131
|
+
|
|
132
|
+
if (!dispatcher) return fetch(input, init);
|
|
155
133
|
|
|
156
|
-
const proxied = (
|
|
157
|
-
input: RequestInfo | URL,
|
|
158
|
-
init?: RequestInit,
|
|
159
|
-
): Promise<Response> => {
|
|
160
|
-
// Some clients (notably axios's fetch adapter) call fetch with a `Request`
|
|
161
|
-
// object. undici's fetch can't consume a foreign Request instance (it tries
|
|
162
|
-
// to coerce it to a URL — "Failed to parse URL from [object Request]"), so
|
|
163
|
-
// normalise a Request into (url, init), carrying over its fields.
|
|
164
134
|
let target: string | URL;
|
|
165
135
|
let opts: Record<string, unknown>;
|
|
136
|
+
|
|
166
137
|
if (typeof Request !== "undefined" && input instanceof Request) {
|
|
167
138
|
target = input.url;
|
|
168
139
|
opts = {
|
|
@@ -175,37 +146,28 @@ export function createProxyFetch(
|
|
|
175
146
|
...init,
|
|
176
147
|
dispatcher,
|
|
177
148
|
};
|
|
178
|
-
// A streamed request body requires half-duplex in undici.
|
|
179
149
|
if (input.body != null) opts.duplex = "half";
|
|
180
150
|
} else {
|
|
181
151
|
target = input as string | URL;
|
|
182
152
|
opts = { ...init, dispatcher };
|
|
183
153
|
}
|
|
154
|
+
|
|
155
|
+
if (useEgress && egressIsPlainHttp) {
|
|
156
|
+
target = downgradeToHttp(target);
|
|
157
|
+
}
|
|
158
|
+
|
|
184
159
|
return undiciFetch(
|
|
185
160
|
target as unknown as Parameters<typeof undiciFetch>[0],
|
|
186
161
|
opts as unknown as Parameters<typeof undiciFetch>[1],
|
|
187
162
|
) as unknown as Promise<Response>;
|
|
188
163
|
};
|
|
189
|
-
|
|
190
|
-
return proxied as typeof fetch;
|
|
191
164
|
}
|
|
192
165
|
|
|
193
|
-
/**
|
|
194
|
-
* Replace `globalThis.fetch` so existing bare `fetch(...)` calls — and any SDK
|
|
195
|
-
* that uses the global fetch by default, including supabase-js — route through
|
|
196
|
-
* the configured proxy with no other code changes. No-op when no proxy is
|
|
197
|
-
* configured.
|
|
198
|
-
*
|
|
199
|
-
* Returns a function that restores the original `fetch`.
|
|
200
|
-
*/
|
|
201
166
|
export function installProxyFetch(options: ProxyFetchOptions = {}): () => void {
|
|
202
|
-
const
|
|
203
|
-
if (
|
|
204
|
-
return () => {};
|
|
205
|
-
}
|
|
206
|
-
|
|
167
|
+
const proxied = createProxyFetch(options);
|
|
168
|
+
if (proxied === fetch) return () => {};
|
|
207
169
|
const original = globalThis.fetch;
|
|
208
|
-
globalThis.fetch =
|
|
170
|
+
globalThis.fetch = proxied;
|
|
209
171
|
return () => {
|
|
210
172
|
globalThis.fetch = original;
|
|
211
173
|
};
|