@provian/mcp 1.2.3 → 1.3.0

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.
Files changed (35) hide show
  1. package/CHANGELOG.md +6 -2
  2. package/README.md +1 -1
  3. package/dist/core/egress-guard.d.ts +39 -0
  4. package/dist/core/egress-guard.d.ts.map +1 -0
  5. package/dist/core/egress-guard.js +303 -0
  6. package/dist/core/egress-guard.js.map +1 -0
  7. package/dist/core/guarded-fetch.d.ts +46 -0
  8. package/dist/core/guarded-fetch.d.ts.map +1 -0
  9. package/dist/core/guarded-fetch.js +236 -0
  10. package/dist/core/guarded-fetch.js.map +1 -0
  11. package/dist/core/pinned-agent.d.ts +61 -0
  12. package/dist/core/pinned-agent.d.ts.map +1 -0
  13. package/dist/core/pinned-agent.js +125 -0
  14. package/dist/core/pinned-agent.js.map +1 -0
  15. package/dist/core/pinned-connection.d.ts +15 -0
  16. package/dist/core/pinned-connection.d.ts.map +1 -0
  17. package/dist/core/pinned-connection.js +172 -0
  18. package/dist/core/pinned-connection.js.map +1 -0
  19. package/dist/core/protocol-pointer-pin.generated.d.ts +6 -0
  20. package/dist/core/protocol-pointer-pin.generated.d.ts.map +1 -0
  21. package/dist/core/protocol-pointer-pin.generated.js +6 -0
  22. package/dist/core/protocol-pointer-pin.generated.js.map +1 -0
  23. package/dist/core/protocol-pointer.d.ts +25 -0
  24. package/dist/core/protocol-pointer.d.ts.map +1 -0
  25. package/dist/core/protocol-pointer.js +132 -0
  26. package/dist/core/protocol-pointer.js.map +1 -0
  27. package/dist/core/protocol-runtime.d.ts +4 -0
  28. package/dist/core/protocol-runtime.d.ts.map +1 -0
  29. package/dist/core/protocol-runtime.js +31 -0
  30. package/dist/core/protocol-runtime.js.map +1 -0
  31. package/dist/core/tools.d.ts +1 -14
  32. package/dist/core/tools.d.ts.map +1 -1
  33. package/dist/core/tools.js +35 -56
  34. package/dist/core/tools.js.map +1 -1
  35. package/package.json +5 -3
@@ -0,0 +1,236 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DOH_RESOLVERS = exports.EgressRefusedError = void 0;
4
+ exports.clearDohCache = clearDohCache;
5
+ exports.resolveViaDoh = resolveViaDoh;
6
+ exports.isFakeIpAnswer = isFakeIpAnswer;
7
+ exports.egressResolver = egressResolver;
8
+ exports.guardedFetch = guardedFetch;
9
+ exports.guardedFetcher = guardedFetcher;
10
+ /** The sole SDK HTTP egress transport: validated DNS bound to the socket, redirects refused. */
11
+ const pinned_connection_1 = require("./pinned-connection");
12
+ const pinned_agent_1 = require("./pinned-agent");
13
+ const node_net_1 = require("node:net");
14
+ const promises_1 = require("node:dns/promises");
15
+ const egress_guard_1 = require("./egress-guard");
16
+ /** The guard refused the destination before any byte left the machine. */
17
+ class EgressRefusedError extends Error {
18
+ constructor(code, reason) {
19
+ super(`egress refused: ${code}: ${reason}`);
20
+ this.name = 'EgressRefusedError';
21
+ this.code = `EGRESS_${code ?? 'REFUSED'}`;
22
+ }
23
+ }
24
+ exports.EgressRefusedError = EgressRefusedError;
25
+ function abortable(promise, signal) {
26
+ if (!signal)
27
+ return promise;
28
+ if (signal.aborted)
29
+ return Promise.reject(signal.reason);
30
+ return new Promise((resolve, reject) => {
31
+ const abort = () => reject(signal.reason);
32
+ signal.addEventListener('abort', abort, { once: true });
33
+ promise.then(resolve, reject).finally(() => signal.removeEventListener('abort', abort));
34
+ });
35
+ }
36
+ /**
37
+ * DNS-over-HTTPS resolvers, addressed by IP so reaching them needs no DNS. Each serves the
38
+ * dns-json shape at the path named here, and each certificate carries its IP as a SAN.
39
+ */
40
+ exports.DOH_RESOLVERS = [
41
+ 'https://1.1.1.1/dns-query',
42
+ 'https://8.8.8.8/resolve',
43
+ ];
44
+ const DOH_TIMEOUT_MS = 8000;
45
+ /**
46
+ * Resolve `hostname` over DNS-over-HTTPS, carried by the same forward proxy as the request.
47
+ *
48
+ * A box whose only egress is a forward proxy often has no DNS of its own (a hosted agent's
49
+ * runtime cell: `getaddrinfo` answers EAI_AGAIN). curl hands the name to the proxy; the guard
50
+ * cannot, because it must check the destination's addresses and pin the tunnel to them
51
+ * (Security Invariant #6). So the guard asks a public resolver, through the proxy, over TLS —
52
+ * and the answer is checked against the blocklist and pinned exactly like a local answer.
53
+ * Returns [] when no resolver answers (the guard then refuses: unknown = block).
54
+ */
55
+ /** Successful DNS-over-HTTPS answers, kept for their TTL (at most 5 minutes). */
56
+ const DOH_CACHE_MAX_MS = 5 * 60 * 1000;
57
+ const dohCache = new Map();
58
+ /** Test seam: forget cached DNS-over-HTTPS answers. */
59
+ function clearDohCache() { dohCache.clear(); }
60
+ async function resolveViaDoh(hostname, fetchFn) {
61
+ const key = hostname.toLowerCase();
62
+ const cached = dohCache.get(key);
63
+ if (cached && cached.untilMs > Date.now())
64
+ return [...cached.ips];
65
+ for (const base of exports.DOH_RESOLVERS) {
66
+ for (const type of ['A', 'AAAA']) {
67
+ try {
68
+ const res = await guardedFetch(`${base}?name=${encodeURIComponent(hostname)}&type=${type}`, { headers: { accept: 'application/dns-json' }, signal: AbortSignal.timeout(DOH_TIMEOUT_MS) }, { fetchFn });
69
+ if (!res.ok) {
70
+ void res.body?.cancel().catch(() => undefined);
71
+ continue;
72
+ }
73
+ const body = await res.json();
74
+ if (body.Status !== 0 || !Array.isArray(body.Answer))
75
+ continue;
76
+ const want = type === 'A' ? 1 : 28;
77
+ const records = body.Answer
78
+ .filter((a) => typeof a === 'object' && a !== null
79
+ && a.type === want && typeof a.data === 'string')
80
+ .filter(a => (0, node_net_1.isIP)(a.data) === (type === 'A' ? 4 : 6));
81
+ const ips = records.map(a => a.data);
82
+ if (ips.length) {
83
+ const ttls = records.map(a => (typeof a.TTL === 'number' && a.TTL >= 0 ? a.TTL * 1000 : 0));
84
+ const ttlMs = Math.min(DOH_CACHE_MAX_MS, ...ttls);
85
+ if (ttlMs > 0)
86
+ dohCache.set(key, { ips: [...ips], untilMs: Date.now() + ttlMs });
87
+ return ips;
88
+ }
89
+ }
90
+ catch { /* next resolver */ }
91
+ }
92
+ }
93
+ return [];
94
+ }
95
+ /**
96
+ * A fake-IP gateway's answer: every name resolves into 198.18.0.0/15 (the benchmark range,
97
+ * RFC 2544), and the gateway maps that address back to the name when the box connects. Such an
98
+ * answer says nothing about where the name really points, so the guard cannot check it.
99
+ */
100
+ function isFakeIpAnswer(ips) {
101
+ if (ips.length === 0)
102
+ return false;
103
+ return ips.every((ip) => {
104
+ const m = /^(?:::ffff:)?198\.(18|19)\.(\d{1,3})\.(\d{1,3})$/i.exec(ip);
105
+ return m !== null;
106
+ });
107
+ }
108
+ /**
109
+ * The resolver the guard uses for `url`, so a destination's REAL addresses are what it checks
110
+ * and pins (Security Invariant #6):
111
+ * - the box's own DNS answer, when it is a real answer (any blocked address in it refuses);
112
+ * - DNS-over-HTTPS when that answer is a fake-IP gateway's (all in 198.18.0.0/15): the real
113
+ * addresses are checked and the connection is pinned to them, never to the gateway's
114
+ * stand-in address;
115
+ * - DNS-over-HTTPS through the forward proxy when the box has no DNS at all and a proxy
116
+ * carries the request. With no proxy and no DNS: no address, refused (unknown = block).
117
+ */
118
+ function egressResolver(url, fetchFn) {
119
+ let proxied = false;
120
+ let schemeProxySet = false;
121
+ // The variable names in effect, as proxyFor reads them (lowercase wins), so a message names
122
+ // the one the person must change.
123
+ let proxyVar = 'HTTPS_PROXY';
124
+ const noProxyVar = process.env.no_proxy ? 'no_proxy' : 'NO_PROXY';
125
+ try {
126
+ const parsed = new URL(url);
127
+ proxied = Boolean((0, pinned_connection_1.proxyFor)(parsed));
128
+ const https = parsed.protocol === 'https:';
129
+ proxyVar = https ? (process.env.https_proxy ? 'https_proxy' : 'HTTPS_PROXY') : (process.env.http_proxy ? 'http_proxy' : 'HTTP_PROXY');
130
+ // The proxy that would carry this scheme (as proxyFor reads it), before NO_PROXY applies.
131
+ schemeProxySet = Boolean(https
132
+ ? process.env.https_proxy || process.env.HTTPS_PROXY
133
+ : process.env.http_proxy || process.env.HTTP_PROXY);
134
+ }
135
+ catch {
136
+ proxied = false;
137
+ }
138
+ const proxyExcluded = !proxied && schemeProxySet;
139
+ return async (hostname) => {
140
+ let local = [];
141
+ try {
142
+ local = (await (0, promises_1.lookup)(hostname, { all: true })).map(r => r.address);
143
+ }
144
+ catch {
145
+ local = [];
146
+ }
147
+ if (local.length && !isFakeIpAnswer(local))
148
+ return local;
149
+ if (local.length === 0 && !proxied) {
150
+ throw new Error(proxyExcluded
151
+ ? `no answer from this machine's DNS; a proxy is set but ${noProxyVar} sends this host direct, so no DNS-over-HTTPS lookup was tried`
152
+ : 'no answer from this machine\'s DNS; no proxy set, so no DNS-over-HTTPS lookup was tried');
153
+ }
154
+ if (proxied && !(0, pinned_connection_1.proxyUrlOf)((0, pinned_connection_1.proxyFor)(new URL(url)) ?? '')) {
155
+ throw new Error(`${local.length ? 'this machine\'s DNS gave only a gateway stand-in address (198.18.0.0/15)' : 'no answer from this machine\'s DNS'}; the proxy setting (${proxyVar}) is not an http:// or https:// proxy, so no DNS-over-HTTPS lookup was tried`);
156
+ }
157
+ const real = await resolveViaDoh(hostname, fetchFn);
158
+ if (real.length === 0) {
159
+ throw new Error(local.length
160
+ ? 'this machine\'s DNS gave only a gateway stand-in address (198.18.0.0/15); the DNS-over-HTTPS lookup (1.1.1.1, 8.8.8.8) got no answer'
161
+ : 'no answer from this machine\'s DNS; the DNS-over-HTTPS lookup through the proxy (1.1.1.1, 8.8.8.8) got no answer');
162
+ }
163
+ return real;
164
+ };
165
+ }
166
+ /**
167
+ * No ambient dispatcher or later DNS lookup can change the approved destination.
168
+ *
169
+ * The pinned dispatcher is a `PinnedAgent` (`pinned-agent.ts`), which every Node's built-in fetch
170
+ * from 20 through 26 accepts — so the default (built-in) fetch, an injected one, and a caller's own
171
+ * wrapper that delegates to the built-in fetch all carry it, on every supported Node.
172
+ */
173
+ async function guardedFetch(input, init, options = {}) {
174
+ const url = input instanceof Request ? input.url : String(input);
175
+ const signal = init?.signal ?? (input instanceof Request ? input.signal : undefined);
176
+ const resolveHost = options.resolveHost ?? egressResolver(url, options.fetchFn);
177
+ const guard = await abortable((0, egress_guard_1.validateEgressTarget)(url, { resolveHost }), signal);
178
+ if (!guard.ok || !guard.resolvedIps?.length)
179
+ throw new EgressRefusedError(guard.code, guard.reason);
180
+ const connect = await abortable((0, pinned_connection_1.pinnedConnector)(new URL(url), guard.resolvedIps, options.resolveHost), signal);
181
+ const dispatcher = new pinned_agent_1.PinnedAgent({ connect });
182
+ const cleanup = () => {
183
+ signal?.removeEventListener('abort', cleanup);
184
+ void dispatcher.destroy().catch(() => undefined);
185
+ };
186
+ signal?.addEventListener('abort', cleanup, { once: true });
187
+ if (signal?.aborted)
188
+ cleanup();
189
+ try {
190
+ const f = options.fetchFn ?? globalThis.fetch;
191
+ const requestInit = { ...init, redirect: 'manual', dispatcher };
192
+ const res = await abortable(f(input, requestInit), signal);
193
+ if (res.type === 'opaqueredirect' || (res.status >= 300 && res.status < 400)) {
194
+ void res.body?.cancel().catch(() => undefined);
195
+ throw new Error('egress redirect refused');
196
+ }
197
+ if (!res.body) {
198
+ cleanup();
199
+ return res;
200
+ }
201
+ const reader = res.body.getReader();
202
+ const body = new ReadableStream({
203
+ async pull(controller) {
204
+ try {
205
+ const next = await abortable(reader.read(), signal);
206
+ if (next.done) {
207
+ controller.close();
208
+ cleanup();
209
+ }
210
+ else
211
+ controller.enqueue(next.value);
212
+ }
213
+ catch (err) {
214
+ controller.error(err);
215
+ cleanup();
216
+ void reader.cancel().catch(() => undefined);
217
+ }
218
+ },
219
+ cancel(reason) { cleanup(); return reader.cancel(reason); },
220
+ });
221
+ const response = new Response(body, { status: res.status, statusText: res.statusText, headers: res.headers });
222
+ Object.defineProperty(response, 'url', { value: res.url || url });
223
+ return response;
224
+ }
225
+ catch (err) {
226
+ cleanup();
227
+ throw err;
228
+ }
229
+ }
230
+ /** Wrap an injected HTTP implementation as well as the native default. */
231
+ function guardedFetcher(fetchFn, resolveHost) {
232
+ if (fetchFn === guardedFetch && !resolveHost)
233
+ return guardedFetch;
234
+ return (input, init) => guardedFetch(input, init, { fetchFn, resolveHost });
235
+ }
236
+ //# sourceMappingURL=guarded-fetch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"guarded-fetch.js","sourceRoot":"","sources":["../../src/core/guarded-fetch.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * The SDK's pinned dispatcher, accepted by every Node's built-in fetch from 20 through 26.
3
+ *
4
+ * The egress guard pins each request to the addresses it approved by handing fetch a `dispatcher`:
5
+ * an `Agent` from the `undici` package whose connector resolves only to those addresses
6
+ * (`pinned-connection.ts`, Security Invariant #6). Node's built-in fetch is a different undici,
7
+ * bundled with Node, and it calls `dispatcher.dispatch(options, handler)`:
8
+ * - Node 20 / 22 / 24 (undici 6 and 7) hand over a handler with the classic callbacks
9
+ * (`onConnect`, `onHeaders`, `onData`, `onComplete`, `onError`) — the `undici@6` Agent's own API;
10
+ * - Node 26 (undici 8) hands over a handler with only the controller callbacks
11
+ * (`onRequestStart`, `onResponseStart`, `onResponseData`, `onResponseEnd`, `onResponseError`),
12
+ * which `undici@6` refuses (`UND_ERR_INVALID_ARG`, "invalid onError method") — so before this
13
+ * module every SDK egress through the built-in fetch, or through a caller's wrapper of it,
14
+ * failed on Node 26.
15
+ *
16
+ * `PinnedAgent` is that same `undici@6` Agent, connector and all, whose `dispatch` translates a
17
+ * controller-style handler into the classic one — exactly the bridge `undici@7`'s own dispatcher
18
+ * applies (`lib/handler/unwrap-handler.js`, MIT), with a header parser that never writes through a
19
+ * prototype. A classic handler passes through untouched, so Node 20–24 behave byte-for-byte as before.
20
+ * Nothing about WHERE a request may connect changes here: the connector is the pinned one.
21
+ */
22
+ import type { Duplex } from 'node:stream';
23
+ import { Agent, type Dispatcher } from 'undici';
24
+ /** A parsed header block: lowercase name → value(s), on an object with no prototype to write through. */
25
+ export type ParsedHeaders = Record<string, string | string[]>;
26
+ /** The controller a controller-style handler drives the request with (undici ≥7 / Node 26 fetch). */
27
+ export interface DispatchController {
28
+ abort(reason?: Error): void;
29
+ pause(): void;
30
+ resume(): void;
31
+ readonly aborted: boolean;
32
+ readonly paused: boolean;
33
+ readonly reason: Error | null;
34
+ rawHeaders: Buffer[] | string[] | null;
35
+ rawTrailers: string[] | Buffer[] | null;
36
+ }
37
+ /** The controller-style callbacks (undici ≥7's handler API). */
38
+ export interface ControllerDispatchHandler {
39
+ onRequestStart?(controller: DispatchController, context: unknown): void;
40
+ onRequestUpgrade?(controller: DispatchController, statusCode: number, headers: ParsedHeaders, socket: Duplex): void;
41
+ onResponseStarted?(): void;
42
+ onResponseStart?(controller: DispatchController, statusCode: number, headers: ParsedHeaders, statusMessage?: string): void;
43
+ onResponseData?(controller: DispatchController, chunk: Buffer): void;
44
+ onResponseEnd?(controller: DispatchController, trailers: ParsedHeaders): void;
45
+ onResponseError?(controller: DispatchController, error: Error): void;
46
+ }
47
+ /** True when `handler` speaks the controller API (undici ≥7's own test: an `onRequestStart`). */
48
+ export declare function isControllerHandler(handler: unknown): handler is ControllerDispatchHandler;
49
+ /**
50
+ * Raw header pairs → `{ name: value | values }`, decoded latin1 as undici ≥7 does. The result has no
51
+ * prototype and every name is defined as an own property, so `__proto__`, `constructor` or any other
52
+ * name from a server can neither pollute a prototype nor break the parse.
53
+ */
54
+ export declare function parseRawHeaderPairs(raw: ReadonlyArray<Buffer | string | (Buffer | string)[]> | null | undefined): ParsedHeaders;
55
+ /** A controller-style handler as the classic callbacks the `undici@6` Agent drives (undici ≥7's UnwrapHandler). */
56
+ export declare function classicHandlerFor(handler: ControllerDispatchHandler): Dispatcher.DispatchHandlers;
57
+ /** The pinned `undici@6` Agent, speaking both handler APIs (see the module comment). */
58
+ export declare class PinnedAgent extends Agent {
59
+ dispatch(options: Agent.DispatchOptions, handler: Dispatcher.DispatchHandlers): boolean;
60
+ }
61
+ //# sourceMappingURL=pinned-agent.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pinned-agent.d.ts","sourceRoot":"","sources":["../../src/core/pinned-agent.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,KAAK,EAAE,KAAK,UAAU,EAAE,MAAM,QAAQ,CAAC;AAEhD,yGAAyG;AACzG,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;AAE9D,qGAAqG;AACrG,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,MAAM,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC;IAC5B,KAAK,IAAI,IAAI,CAAC;IACd,MAAM,IAAI,IAAI,CAAC;IACf,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,KAAK,GAAG,IAAI,CAAC;IAC9B,UAAU,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,GAAG,IAAI,CAAC;IACvC,WAAW,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,GAAG,IAAI,CAAC;CACzC;AAED,gEAAgE;AAChE,MAAM,WAAW,yBAAyB;IACxC,cAAc,CAAC,CAAC,UAAU,EAAE,kBAAkB,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IACxE,gBAAgB,CAAC,CAAC,UAAU,EAAE,kBAAkB,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACpH,iBAAiB,CAAC,IAAI,IAAI,CAAC;IAC3B,eAAe,CAAC,CAAC,UAAU,EAAE,kBAAkB,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,EAAE,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3H,cAAc,CAAC,CAAC,UAAU,EAAE,kBAAkB,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrE,aAAa,CAAC,CAAC,UAAU,EAAE,kBAAkB,EAAE,QAAQ,EAAE,aAAa,GAAG,IAAI,CAAC;IAC9E,eAAe,CAAC,CAAC,UAAU,EAAE,kBAAkB,EAAE,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;CACtE;AAED,iGAAiG;AACjG,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,IAAI,yBAAyB,CAG1F;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,aAAa,CAAC,MAAM,GAAG,MAAM,GAAG,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC,GAAG,IAAI,GAAG,SAAS,GAAG,aAAa,CAgB/H;AA2BD,mHAAmH;AACnH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,yBAAyB,GAAG,UAAU,CAAC,gBAAgB,CAoCjG;AAED,wFAAwF;AACxF,qBAAa,WAAY,SAAQ,KAAK;IAC3B,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,eAAe,EAAE,OAAO,EAAE,UAAU,CAAC,gBAAgB,GAAG,OAAO;CAGjG"}
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
3
+ if (kind === "m") throw new TypeError("Private method is not writable");
4
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
5
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
6
+ return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
7
+ };
8
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
9
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
10
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
11
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
12
+ };
13
+ var _BridgeController_paused, _BridgeController_aborted, _BridgeController_reason, _BridgeController_abort;
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.PinnedAgent = void 0;
16
+ exports.isControllerHandler = isControllerHandler;
17
+ exports.parseRawHeaderPairs = parseRawHeaderPairs;
18
+ exports.classicHandlerFor = classicHandlerFor;
19
+ const undici_1 = require("undici");
20
+ /** True when `handler` speaks the controller API (undici ≥7's own test: an `onRequestStart`). */
21
+ function isControllerHandler(handler) {
22
+ return typeof handler === 'object' && handler !== null
23
+ && typeof handler.onRequestStart === 'function';
24
+ }
25
+ /**
26
+ * Raw header pairs → `{ name: value | values }`, decoded latin1 as undici ≥7 does. The result has no
27
+ * prototype and every name is defined as an own property, so `__proto__`, `constructor` or any other
28
+ * name from a server can neither pollute a prototype nor break the parse.
29
+ */
30
+ function parseRawHeaderPairs(raw) {
31
+ const out = Object.create(null);
32
+ if (!raw)
33
+ return out;
34
+ const text = (v) => (typeof v === 'string' ? v : v.toString('latin1'));
35
+ for (let i = 0; i + 1 < raw.length; i += 2) {
36
+ const nameRaw = raw[i], valueRaw = raw[i + 1];
37
+ if (Array.isArray(nameRaw))
38
+ continue;
39
+ const name = text(nameRaw).toLowerCase();
40
+ const values = Array.isArray(valueRaw) ? valueRaw.map(text) : [text(valueRaw)];
41
+ const prior = Object.prototype.hasOwnProperty.call(out, name) ? out[name] : undefined;
42
+ const next = prior === undefined
43
+ ? (values.length === 1 ? values[0] : values)
44
+ : [...(typeof prior === 'string' ? [prior] : prior), ...values];
45
+ Object.defineProperty(out, name, { value: next, enumerable: true, configurable: true, writable: true });
46
+ }
47
+ return out;
48
+ }
49
+ class BridgeController {
50
+ constructor(abort) {
51
+ _BridgeController_paused.set(this, false);
52
+ _BridgeController_aborted.set(this, false);
53
+ _BridgeController_reason.set(this, null);
54
+ _BridgeController_abort.set(this, void 0);
55
+ this.resumeRead = null;
56
+ this.rawHeaders = null;
57
+ this.rawTrailers = null;
58
+ __classPrivateFieldSet(this, _BridgeController_abort, abort, "f");
59
+ }
60
+ pause() { __classPrivateFieldSet(this, _BridgeController_paused, true, "f"); }
61
+ resume() {
62
+ if (__classPrivateFieldGet(this, _BridgeController_paused, "f")) {
63
+ __classPrivateFieldSet(this, _BridgeController_paused, false, "f");
64
+ this.resumeRead?.();
65
+ }
66
+ }
67
+ abort(reason) {
68
+ if (__classPrivateFieldGet(this, _BridgeController_aborted, "f"))
69
+ return;
70
+ __classPrivateFieldSet(this, _BridgeController_aborted, true, "f");
71
+ __classPrivateFieldSet(this, _BridgeController_reason, reason ?? null, "f");
72
+ __classPrivateFieldGet(this, _BridgeController_abort, "f").call(this, reason);
73
+ }
74
+ get aborted() { return __classPrivateFieldGet(this, _BridgeController_aborted, "f"); }
75
+ get paused() { return __classPrivateFieldGet(this, _BridgeController_paused, "f"); }
76
+ get reason() { return __classPrivateFieldGet(this, _BridgeController_reason, "f"); }
77
+ }
78
+ _BridgeController_paused = new WeakMap(), _BridgeController_aborted = new WeakMap(), _BridgeController_reason = new WeakMap(), _BridgeController_abort = new WeakMap();
79
+ /** A controller-style handler as the classic callbacks the `undici@6` Agent drives (undici ≥7's UnwrapHandler). */
80
+ function classicHandlerFor(handler) {
81
+ let controller = null;
82
+ const live = () => (controller ?? (controller = new BridgeController(() => undefined)));
83
+ return {
84
+ onConnect(abort, context) {
85
+ controller = new BridgeController(abort);
86
+ handler.onRequestStart?.(controller, context);
87
+ },
88
+ onResponseStarted() { handler.onResponseStarted?.(); },
89
+ onUpgrade(statusCode, rawHeaders, socket) {
90
+ const c = live();
91
+ c.rawHeaders = rawHeaders;
92
+ handler.onRequestUpgrade?.(c, statusCode, parseRawHeaderPairs(rawHeaders), socket);
93
+ },
94
+ onHeaders(statusCode, rawHeaders, resume, statusText) {
95
+ const c = live();
96
+ c.resumeRead = resume;
97
+ c.rawHeaders = rawHeaders;
98
+ handler.onResponseStart?.(c, statusCode, parseRawHeaderPairs(rawHeaders), statusText);
99
+ return !c.paused;
100
+ },
101
+ onData(chunk) {
102
+ const c = live();
103
+ handler.onResponseData?.(c, chunk);
104
+ return !c.paused;
105
+ },
106
+ onComplete(rawTrailers) {
107
+ const c = live();
108
+ c.rawTrailers = rawTrailers;
109
+ handler.onResponseEnd?.(c, parseRawHeaderPairs(rawTrailers));
110
+ },
111
+ onError(err) {
112
+ if (typeof handler.onResponseError !== 'function')
113
+ throw new Error('invalid onError method');
114
+ handler.onResponseError(live(), err);
115
+ },
116
+ };
117
+ }
118
+ /** The pinned `undici@6` Agent, speaking both handler APIs (see the module comment). */
119
+ class PinnedAgent extends undici_1.Agent {
120
+ dispatch(options, handler) {
121
+ return super.dispatch(options, isControllerHandler(handler) ? classicHandlerFor(handler) : handler);
122
+ }
123
+ }
124
+ exports.PinnedAgent = PinnedAgent;
125
+ //# sourceMappingURL=pinned-agent.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pinned-agent.js","sourceRoot":"","sources":["../../src/core/pinned-agent.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ import type { LookupFunction } from 'node:net';
2
+ import { buildConnector } from 'undici';
3
+ import type { HostResolver } from './egress-guard';
4
+ export declare function pinnedLookup(hostname: string, addresses: readonly string[]): LookupFunction;
5
+ /** The forward proxy that carries a request to `url`, honouring NO_PROXY; undefined when none. */
6
+ export declare function proxyFor(url: URL): string | undefined;
7
+ /**
8
+ * The forward proxy as a URL, read as curl reads it: a value with no scheme
9
+ * (`proxy.local:3128`, `127.0.0.1:3128`) means http://. Null for anything other than an
10
+ * http:// or https:// proxy (Provian speaks only HTTP CONNECT to a proxy).
11
+ */
12
+ export declare function proxyUrlOf(value: string): URL | null;
13
+ /** The target is approved and the proxy socket pinned; CONNECT names an IP, never remote DNS. */
14
+ export declare function pinnedConnector(url: URL, addresses: readonly string[], resolveHost?: HostResolver): Promise<buildConnector.connector>;
15
+ //# sourceMappingURL=pinned-connection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pinned-connection.d.ts","sourceRoot":"","sources":["../../src/core/pinned-connection.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC/C,OAAO,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAC;AAExC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAEnD,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,MAAM,EAAE,GAAG,cAAc,CAS3F;AAED,kGAAkG;AAClG,wBAAgB,QAAQ,CAAC,GAAG,EAAE,GAAG,GAAG,MAAM,GAAG,SAAS,CAYrD;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,GAAG,IAAI,CAMpD;AAqBD,iGAAiG;AACjG,wBAAsB,eAAe,CAAC,GAAG,EAAE,GAAG,EAAE,SAAS,EAAE,SAAS,MAAM,EAAE,EAAE,WAAW,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,cAAc,CAAC,SAAS,CAAC,CAkC3I"}
@@ -0,0 +1,172 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.pinnedLookup = pinnedLookup;
37
+ exports.proxyFor = proxyFor;
38
+ exports.proxyUrlOf = proxyUrlOf;
39
+ exports.pinnedConnector = pinnedConnector;
40
+ /** Fresh, DNS-pinned sockets, including approved forward-proxy CONNECT tunnels. */
41
+ const http = __importStar(require("node:http"));
42
+ const https = __importStar(require("node:https"));
43
+ const node_net_1 = require("node:net");
44
+ const undici_1 = require("undici");
45
+ const promises_1 = require("node:dns/promises");
46
+ function pinnedLookup(hostname, addresses) {
47
+ return (host, opts, callback) => {
48
+ if (host.replace(/^\[|\]$/g, '') !== hostname.replace(/^\[|\]$/g, '')) {
49
+ callback(new Error('egress lookup host mismatch'), '', 4);
50
+ return;
51
+ }
52
+ const matches = addresses.map(address => ({ address, family: (0, node_net_1.isIP)(address) }))
53
+ .filter(a => !opts.family || opts.family === a.family);
54
+ if (!matches.length) {
55
+ callback(new Error('egress address family unavailable'), '', 4);
56
+ return;
57
+ }
58
+ if (opts.all)
59
+ callback(null, matches);
60
+ else
61
+ callback(null, matches[0].address, matches[0].family);
62
+ };
63
+ }
64
+ /** The forward proxy that carries a request to `url`, honouring NO_PROXY; undefined when none. */
65
+ function proxyFor(url) {
66
+ const excluded = (process.env.no_proxy || process.env.NO_PROXY || '').split(',').some(raw => {
67
+ const entry = raw.trim().toLowerCase();
68
+ if (!entry)
69
+ return false;
70
+ if (entry === '*')
71
+ return true;
72
+ const host = url.hostname.toLowerCase();
73
+ if (entry === host || entry === url.host.toLowerCase())
74
+ return true;
75
+ const suffix = entry.replace(/^\*?\./, '');
76
+ return host === suffix || host.endsWith(`.${suffix}`);
77
+ });
78
+ if (excluded)
79
+ return undefined;
80
+ return url.protocol === 'https:' ? process.env.https_proxy || process.env.HTTPS_PROXY : process.env.http_proxy || process.env.HTTP_PROXY;
81
+ }
82
+ /**
83
+ * The forward proxy as a URL, read as curl reads it: a value with no scheme
84
+ * (`proxy.local:3128`, `127.0.0.1:3128`) means http://. Null for anything other than an
85
+ * http:// or https:// proxy (Provian speaks only HTTP CONNECT to a proxy).
86
+ */
87
+ function proxyUrlOf(value) {
88
+ const raw = value.trim();
89
+ if (!raw)
90
+ return null;
91
+ let parsed;
92
+ try {
93
+ parsed = new URL(/^[a-z][a-z0-9+.-]*:\/\//i.test(raw) ? raw : `http://${raw}`);
94
+ }
95
+ catch {
96
+ return null;
97
+ }
98
+ return parsed.protocol === 'http:' || parsed.protocol === 'https:' ? parsed : null;
99
+ }
100
+ /**
101
+ * The proxy's addresses. The forward proxy is the OPERATOR's own egress hop — an environment
102
+ * variable only the box's operator sets, and commonly a host on the local network — so the SSRF
103
+ * block does not apply to it: what the guard approved is the TARGET, and CONNECT names the approved
104
+ * target IP. The proxy host is still resolved here, once, and the proxy socket pinned to that
105
+ * answer, so nothing re-resolves it in transit. (Before 2026-10-17 the proxy went through the
106
+ * target guard, which refused every private-network proxy — the corporate deployment.)
107
+ */
108
+ async function resolveProxyAddresses(hostname, resolveHost) {
109
+ const bare = hostname.replace(/^\[|\]$/g, '');
110
+ if ((0, node_net_1.isIP)(bare))
111
+ return [bare];
112
+ try {
113
+ const ips = resolveHost ? await resolveHost(bare) : (await (0, promises_1.lookup)(bare, { all: true })).map(r => r.address);
114
+ return ips.filter(ip => (0, node_net_1.isIP)(ip) !== 0);
115
+ }
116
+ catch {
117
+ return [];
118
+ }
119
+ }
120
+ /** The target is approved and the proxy socket pinned; CONNECT names an IP, never remote DNS. */
121
+ async function pinnedConnector(url, addresses, resolveHost) {
122
+ const direct = (0, undici_1.buildConnector)({ lookup: pinnedLookup(url.hostname, addresses) });
123
+ const proxyValue = proxyFor(url);
124
+ if (!proxyValue)
125
+ return direct;
126
+ const proxy = proxyUrlOf(proxyValue);
127
+ if (!proxy)
128
+ throw new Error('egress proxy protocol refused');
129
+ const proxyIps = await resolveProxyAddresses(proxy.hostname, resolveHost);
130
+ if (proxyIps.length === 0)
131
+ throw new Error('egress proxy refused');
132
+ return (options, callback) => {
133
+ const agent = proxy.protocol === 'https:' ? new https.Agent({ keepAlive: false }) : new http.Agent({ keepAlive: false });
134
+ const address = addresses[0], port = options.port || (url.protocol === 'https:' ? '443' : '80');
135
+ const authority = `${(0, node_net_1.isIP)(address) === 6 ? `[${address}]` : address}:${port}`;
136
+ const headers = { host: authority };
137
+ if (proxy.username || proxy.password)
138
+ headers['proxy-authorization'] = `Basic ${Buffer.from(`${decodeURIComponent(proxy.username)}:${decodeURIComponent(proxy.password)}`).toString('base64')}`;
139
+ const transport = proxy.protocol === 'https:' ? https : http;
140
+ const request = transport.request(proxy, { method: 'CONNECT', path: authority, headers, agent, lookup: pinnedLookup(proxy.hostname, proxyIps), timeout: 10000 });
141
+ let finished = false;
142
+ const finish = (err, socket) => {
143
+ if (finished)
144
+ return;
145
+ finished = true;
146
+ if (err) {
147
+ agent.destroy();
148
+ callback(err, null);
149
+ }
150
+ else
151
+ callback(null, socket);
152
+ };
153
+ request.on('timeout', () => request.destroy(new Error('egress proxy connection timed out')));
154
+ request.on('error', err => finish(err, null));
155
+ request.on('connect', (response, socket, head) => {
156
+ if (response.statusCode !== 200 || head.length) {
157
+ socket.destroy();
158
+ finish(new Error('egress proxy tunnel refused'), null);
159
+ return;
160
+ }
161
+ // An IP-literal target (the DNS-over-HTTPS resolvers) sends no SNI (RFC 6066) and is
162
+ // verified against the certificate's IP SAN instead.
163
+ const bare = url.hostname.replace(/^\[|\]$/g, '');
164
+ if (url.protocol === 'https:')
165
+ direct({ ...options, httpSocket: socket, ...((0, node_net_1.isIP)(bare) ? {} : { servername: url.hostname }) }, finish);
166
+ else
167
+ finish(null, socket);
168
+ });
169
+ request.end();
170
+ };
171
+ }
172
+ //# sourceMappingURL=pinned-connection.js.map