@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.
@@ -1,4 +1,4 @@
1
1
 
2
- > @introspection-sdk/introspection-proxy@0.1.0 build /home/runner/work/introspection-js-sdk/introspection-js-sdk/packages/introspection-proxy
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
- * Build an undici {@link Dispatcher} for outbound HTTP based on the
29
- * environment (or explicit {@link ProxyFetchOptions}). Returns `undefined`
30
- * when no proxy is configured, so callers can fall back to the default fetch.
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
@@ -1 +1 @@
1
- {"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"AAuCA,OAAO,EAML,KAAK,UAAU,EAChB,MAAM,QAAQ,CAAC;AAEhB,MAAM,WAAW,iBAAiB;IAChC;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;;;;;OASG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAMD;;;;;GAKG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,GAAG,SAAS,CAS3D;AA4BD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,GAAE,iBAAsB,GAC9B,UAAU,GAAG,SAAS,CAgBxB;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,GAAE,iBAAsB,GAC9B,OAAO,KAAK,CAyCd;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,iBAAsB,GAAG,MAAM,IAAI,CAW7E"}
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
- * These let any `fetch` call — a bare `fetch(...)`, an SDK that accepts a
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
- * Two proxy shapes are supported, selected from the environment:
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
- * - **Egress credential-injection proxy** (`EGRESS_PROXY_URL`): the
12
- * Introspection egress proxy is a plain-HTTP reverse proxy that routes by
13
- * the request's `Host`/`:authority` header and injects the real upstream
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, ProxyAgent, fetch as undiciFetch, } from "undici";
41
- function resolveEgressProxyUrl(options) {
42
- return options.egressProxyUrl ?? process.env.EGRESS_PROXY_URL ?? undefined;
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
- const env = process.env;
52
- return (env.HTTPS_PROXY ||
53
- env.https_proxy ||
54
- env.HTTP_PROXY ||
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
- * Build an undici {@link Dispatcher} for outbound HTTP based on the
82
- * environment (or explicit {@link ProxyFetchOptions}). Returns `undefined`
83
- * when no proxy is configured, so callers can fall back to the default fetch.
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 = resolveEgressProxyUrl(options);
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
- * A `fetch`-compatible function that routes through the configured proxy.
101
- * Drop-in for any client that accepts a `fetch` (e.g. supabase-js
102
- * `createClient(url, key, { global: { fetch: createProxyFetch() } })`).
103
- *
104
- * When no proxy is configured this returns the global `fetch` unchanged, so it
105
- * is safe to use unconditionally (local dev keeps talking directly to APIs).
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 dispatcher = getProxyDispatcher(options);
109
- if (!dispatcher) {
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
- const proxied = (input, init) => {
113
- // Some clients (notably axios's fetch adapter) call fetch with a `Request`
114
- // object. undici's fetch can't consume a foreign Request instance (it tries
115
- // to coerce it to a URL — "Failed to parse URL from [object Request]"), so
116
- // normalise a Request into (url, init), carrying over its fields.
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 dispatcher = getProxyDispatcher(options);
153
- if (!dispatcher) {
126
+ const proxied = createProxyFetch(options);
127
+ if (proxied === fetch)
154
128
  return () => { };
155
- }
156
129
  const original = globalThis.fetch;
157
- globalThis.fetch = createProxyFetch(options);
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,UAAU,CAAC;AACjD,OAAO,EACL,KAAK,EACL,iBAAiB,EACjB,UAAU,EAEV,KAAK,IAAI,WAAW,GAErB,MAAM,QAAQ,CAAC;AAqBhB,SAAS,qBAAqB,CAAC,OAA0B;IACvD,OAAO,OAAO,CAAC,cAAc,IAAI,OAAO,CAAC,GAAG,CAAC,gBAAgB,IAAI,SAAS,CAAC;AAC7E,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,sBAAsB;IACpC,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC;IACxB,OAAO,CACL,GAAG,CAAC,WAAW;QACf,GAAG,CAAC,WAAW;QACf,GAAG,CAAC,UAAU;QACd,GAAG,CAAC,UAAU;QACd,SAAS,CACV,CAAC;AACJ,CAAC;AAED,SAAS,qBAAqB,CAAC,QAAgB;IAC7C,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,CAAC;IAC9B,MAAM,MAAM,GAAG,GAAG,CAAC,QAAQ,KAAK,QAAQ,CAAC;IACzC,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IACrD,MAAM,IAAI,GAAG,GAAG,CAAC,QAAQ,CAAC;IAE1B,4EAA4E;IAC5E,8EAA8E;IAC9E,OAAO,IAAI,KAAK,CAAC;QACf,OAAO,CACL,QAAgC,EAChC,QAAiC;YAEjC,IAAI,MAAM,EAAE,CAAC;gBACX,MAAM,MAAM,GAAG,UAAU,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC;gBAC5D,MAAM,CAAC,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;gBAC3D,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC;YACrD,CAAC;iBAAM,CAAC;gBACN,MAAM,MAAM,GAAG,IAAI,MAAM,EAAE,CAAC;gBAC5B,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;gBACzD,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC;YACrD,CAAC;QACH,CAAC;KACF,CAAC,CAAC;AACL,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAChC,UAA6B,EAAE;IAE/B,MAAM,SAAS,GAAG,qBAAqB,CAAC,OAAO,CAAC,CAAC;IACjD,IAAI,SAAS,EAAE,CAAC;QACd,OAAO,qBAAqB,CAAC,SAAS,CAAC,CAAC;IAC1C,CAAC;IAED,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC;QAC5B,OAAO,IAAI,UAAU,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC;IACjD,CAAC;IAED,IAAI,sBAAsB,EAAE,EAAE,CAAC;QAC7B,kEAAkE;QAClE,OAAO,IAAI,iBAAiB,EAAE,CAAC;IACjC,CAAC;IAED,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,gBAAgB,CAC9B,UAA6B,EAAE;IAE/B,MAAM,UAAU,GAAG,kBAAkB,CAAC,OAAO,CAAC,CAAC;IAC/C,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,OAAO,KAAK,CAAC;IACf,CAAC;IAED,MAAM,OAAO,GAAG,CACd,KAAwB,EACxB,IAAkB,EACC,EAAE;QACrB,2EAA2E;QAC3E,4EAA4E;QAC5E,2EAA2E;QAC3E,kEAAkE;QAClE,IAAI,MAAoB,CAAC;QACzB,IAAI,IAA6B,CAAC;QAClC,IAAI,OAAO,OAAO,KAAK,WAAW,IAAI,KAAK,YAAY,OAAO,EAAE,CAAC;YAC/D,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC;YACnB,IAAI,GAAG;gBACL,MAAM,EAAE,KAAK,CAAC,MAAM;gBACpB,OAAO,EAAE,KAAK,CAAC,OAAO;gBACtB,IAAI,EAAE,KAAK,CAAC,IAAI;gBAChB,MAAM,EAAE,KAAK,CAAC,MAAM;gBACpB,QAAQ,EAAE,KAAK,CAAC,QAAQ;gBACxB,WAAW,EAAE,KAAK,CAAC,WAAW;gBAC9B,GAAG,IAAI;gBACP,UAAU;aACX,CAAC;YACF,0DAA0D;YAC1D,IAAI,KAAK,CAAC,IAAI,IAAI,IAAI;gBAAE,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QAC/C,CAAC;aAAM,CAAC;YACN,MAAM,GAAG,KAAqB,CAAC;YAC/B,IAAI,GAAG,EAAE,GAAG,IAAI,EAAE,UAAU,EAAE,CAAC;QACjC,CAAC;QACD,OAAO,WAAW,CAChB,MAAsD,EACtD,IAAoD,CACrB,CAAC;IACpC,CAAC,CAAC;IAEF,OAAO,OAAuB,CAAC;AACjC,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,iBAAiB,CAAC,UAA6B,EAAE;IAC/D,MAAM,UAAU,GAAG,kBAAkB,CAAC,OAAO,CAAC,CAAC;IAC/C,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,OAAO,GAAG,EAAE,GAAE,CAAC,CAAC;IAClB,CAAC;IAED,MAAM,QAAQ,GAAG,UAAU,CAAC,KAAK,CAAC;IAClC,UAAU,CAAC,KAAK,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;IAC7C,OAAO,GAAG,EAAE;QACV,UAAU,CAAC,KAAK,GAAG,QAAQ,CAAC;IAC9B,CAAC,CAAC;AACJ,CAAC"}
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.1.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
- * These let any `fetch` call — a bare `fetch(...)`, an SDK that accepts a
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
- * Two proxy shapes are supported, selected from the environment:
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
- * - **Egress credential-injection proxy** (`EGRESS_PROXY_URL`): the
12
- * Introspection egress proxy is a plain-HTTP reverse proxy that routes by
13
- * the request's `Host`/`:authority` header and injects the real upstream
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 resolveEgressProxyUrl(options: ProxyFetchOptions): string | undefined {
69
- return options.egressProxyUrl ?? process.env.EGRESS_PROXY_URL ?? undefined;
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
- * Build an undici {@link Dispatcher} for outbound HTTP based on the
117
- * environment (or explicit {@link ProxyFetchOptions}). Returns `undefined`
118
- * when no proxy is configured, so callers can fall back to the default fetch.
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 = resolveEgressProxyUrl(options);
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
- * A `fetch`-compatible function that routes through the configured proxy.
142
- * Drop-in for any client that accepts a `fetch` (e.g. supabase-js
143
- * `createClient(url, key, { global: { fetch: createProxyFetch() } })`).
144
- *
145
- * When no proxy is configured this returns the global `fetch` unchanged, so it
146
- * is safe to use unconditionally (local dev keeps talking directly to APIs).
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 dispatcher = getProxyDispatcher(options);
152
- if (!dispatcher) {
153
- return fetch;
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 dispatcher = getProxyDispatcher(options);
203
- if (!dispatcher) {
204
- return () => {};
205
- }
206
-
167
+ const proxied = createProxyFetch(options);
168
+ if (proxied === fetch) return () => {};
207
169
  const original = globalThis.fetch;
208
- globalThis.fetch = createProxyFetch(options);
170
+ globalThis.fetch = proxied;
209
171
  return () => {
210
172
  globalThis.fetch = original;
211
173
  };