@oh-my-pi/pi-ai 16.1.13 → 16.1.14

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.
@@ -0,0 +1,239 @@
1
+ import * as net from "node:net";
2
+ import * as tls from "node:tls";
3
+ import type { FetchImpl } from "../types";
4
+
5
+ /**
6
+ * Checks if a host is local or cloud metadata, which should always bypass the proxy
7
+ * (e.g. localhost, 127/8, ::1, 169.254.169.254, metadata.google.internal).
8
+ */
9
+ export function isLocalOrMetadataHost(host: string): boolean {
10
+ const lowerHost = host.toLowerCase();
11
+
12
+ // Hostnames: localhost and the cloud metadata service.
13
+ if (lowerHost === "localhost" || lowerHost.endsWith(".localhost") || lowerHost === "metadata.google.internal") {
14
+ return true;
15
+ }
16
+
17
+ // Strip IPv6 brackets before numeric checks.
18
+ const ip = lowerHost.replace(/^\[|\]$/g, "");
19
+
20
+ // IPv4 loopback (127/8), unspecified (0/8), RFC1918 private (10/8, 172.16/12,
21
+ // 192.168/16) and link-local (169.254/16 — covers IMDS 169.254.169.254 and
22
+ // ECS credentials 169.254.170.2). None are reachable through a remote egress
23
+ // proxy, and credential/metadata probes must never leak to one.
24
+ const v4 = ip.match(/^(\d{1,3})\.(\d{1,3})\.\d{1,3}\.\d{1,3}$/);
25
+ if (v4) {
26
+ const a = Number(v4[1]);
27
+ const b = Number(v4[2]);
28
+ if (a === 127 || a === 10 || a === 0) return true;
29
+ if (a === 169 && b === 254) return true;
30
+ if (a === 192 && b === 168) return true;
31
+ if (a === 172 && b >= 16 && b <= 31) return true;
32
+ return false;
33
+ }
34
+
35
+ // IPv6 loopback (::1), unspecified (::), link-local (fe80::/10) and
36
+ // unique-local (fc00::/7 — covers EC2 IPv6 IMDS fd00:ec2::254).
37
+ if (ip === "::1" || ip === "::") return true;
38
+ if (/^fe[89ab][0-9a-f]:/.test(ip)) return true;
39
+ if (/^f[cd][0-9a-f]{2}:/.test(ip)) return true;
40
+
41
+ return false;
42
+ }
43
+
44
+ /**
45
+ * Check if the url should bypass the proxy due to hard-coded localhost/metadata checks
46
+ * or custom NO_PROXY/no_proxy environment variables rules.
47
+ */
48
+ export function shouldBypassProxy(urlObj: URL): boolean {
49
+ if (isLocalOrMetadataHost(urlObj.hostname)) {
50
+ return true;
51
+ }
52
+
53
+ const noProxyVal = Bun.env.NO_PROXY || Bun.env.no_proxy;
54
+ if (!noProxyVal) {
55
+ return false;
56
+ }
57
+
58
+ const rules = noProxyVal
59
+ .split(/[,\s]+/)
60
+ .map(r => r.trim())
61
+ .filter(Boolean);
62
+ const targetHost = urlObj.hostname.toLowerCase();
63
+ const targetPort = urlObj.port || (urlObj.protocol === "https:" ? "443" : "80");
64
+
65
+ for (const rule of rules) {
66
+ if (rule === "*") {
67
+ return true;
68
+ }
69
+
70
+ let ruleHost = rule.toLowerCase();
71
+ let rulePort: string | undefined;
72
+
73
+ if (ruleHost.includes("]:")) {
74
+ const lastColon = ruleHost.lastIndexOf(":");
75
+ rulePort = ruleHost.slice(lastColon + 1);
76
+ ruleHost = ruleHost.slice(0, lastColon);
77
+ } else if (!ruleHost.includes("]") && ruleHost.includes(":")) {
78
+ const lastColon = ruleHost.lastIndexOf(":");
79
+ rulePort = ruleHost.slice(lastColon + 1);
80
+ ruleHost = ruleHost.slice(0, lastColon);
81
+ }
82
+
83
+ // Strip IPv6 brackets
84
+ ruleHost = ruleHost.replace(/^\[|\]$/g, "");
85
+
86
+ if (rulePort && rulePort !== targetPort) {
87
+ continue;
88
+ }
89
+
90
+ // Match host part
91
+ if (ruleHost.startsWith(".")) {
92
+ const suffix = ruleHost;
93
+ const cleanRule = ruleHost.slice(1);
94
+ if (targetHost === cleanRule || targetHost.endsWith(suffix)) {
95
+ return true;
96
+ }
97
+ } else {
98
+ if (targetHost === ruleHost || targetHost.endsWith(`.${ruleHost}`)) {
99
+ return true;
100
+ }
101
+ }
102
+ }
103
+
104
+ return false;
105
+ }
106
+
107
+ const proxyCache = new Map<string, string | undefined>();
108
+
109
+ /** Test seam: clears the provider proxy cache. */
110
+ export function __resetProxyCache(): void {
111
+ proxyCache.clear();
112
+ }
113
+
114
+ /**
115
+ * Normalizes provider id (e.g. github-copilot -> PI_PROXY_GITHUB_COPILOT) and looks it up.
116
+ * If not found, falls back to PI_PROXY. Results are memoized because env values are static
117
+ * for the lifetime of the process and this function is called for every outgoing request.
118
+ */
119
+ export function getProxyForProvider(provider: string): string | undefined {
120
+ if (proxyCache.has(provider)) {
121
+ return proxyCache.get(provider);
122
+ }
123
+
124
+ const normalized = provider.toUpperCase().replace(/[^A-Z0-9]/g, "_");
125
+ const envKey = `PI_PROXY_${normalized}`;
126
+ const value = Bun.env[envKey] || Bun.env.PI_PROXY;
127
+ proxyCache.set(provider, value);
128
+ return value;
129
+ }
130
+
131
+ /**
132
+ * Wraps a fetch implementation to inject proxy options for non-local hosts.
133
+ */
134
+ export function wrapFetchForProxy(fetchImpl: FetchImpl, provider: string): FetchImpl {
135
+ const proxyUrl = getProxyForProvider(provider);
136
+ if (!proxyUrl) {
137
+ return fetchImpl;
138
+ }
139
+
140
+ const wrapped = async (input: string | URL | Request, init?: RequestInit): Promise<Response> => {
141
+ const urlStr = input instanceof Request ? input.url : input.toString();
142
+ let urlObj: URL;
143
+ try {
144
+ urlObj = new URL(urlStr);
145
+ } catch {
146
+ // Fallback to calling fetch unmodified if URL is unparseable
147
+ return fetchImpl(input, init);
148
+ }
149
+
150
+ if (shouldBypassProxy(urlObj)) {
151
+ return fetchImpl(input, init);
152
+ }
153
+
154
+ const mergedInit = { ...(init ?? {}), proxy: proxyUrl };
155
+ return fetchImpl(input, mergedInit);
156
+ };
157
+
158
+ if (fetchImpl.preconnect) {
159
+ wrapped.preconnect = fetchImpl.preconnect;
160
+ }
161
+ return wrapped;
162
+ }
163
+
164
+ /**
165
+ * Tunnel a socket connection through an HTTP CONNECT proxy.
166
+ * This is used specifically to wrap Node's `http2.connect(baseUrl, { createConnection })` for Cursor.
167
+ */
168
+ export async function connectProxiedSocket(proxyUrlStr: string, targetUrlStr: string): Promise<tls.TLSSocket> {
169
+ const proxyUrl = new URL(proxyUrlStr);
170
+ const targetUrl = new URL(targetUrlStr);
171
+
172
+ const useProxySsl = proxyUrl.protocol === "https:";
173
+ const proxyPort = proxyUrl.port ? parseInt(proxyUrl.port, 10) : useProxySsl ? 443 : 80;
174
+ const proxyHost = proxyUrl.hostname;
175
+
176
+ const targetPort = targetUrl.port ? parseInt(targetUrl.port, 10) : 443;
177
+ const targetHost = targetUrl.hostname;
178
+
179
+ const { promise, resolve, reject } = Promise.withResolvers<tls.TLSSocket>();
180
+
181
+ let rawSocket: net.Socket;
182
+ if (useProxySsl) {
183
+ rawSocket = tls.connect({
184
+ host: proxyHost,
185
+ port: proxyPort,
186
+ });
187
+ } else {
188
+ rawSocket = net.connect({
189
+ host: proxyHost,
190
+ port: proxyPort,
191
+ });
192
+ }
193
+
194
+ rawSocket.once("error", reject);
195
+
196
+ const readyEvent = useProxySsl ? "secureConnect" : "connect";
197
+ rawSocket.once(readyEvent, () => {
198
+ let connectReq = `CONNECT ${targetHost}:${targetPort} HTTP/1.1\r\n` + `Host: ${targetHost}:${targetPort}\r\n`;
199
+
200
+ if (proxyUrl.username || proxyUrl.password) {
201
+ const creds = Buffer.from(
202
+ `${decodeURIComponent(proxyUrl.username)}:${decodeURIComponent(proxyUrl.password)}`,
203
+ ).toString("base64");
204
+ connectReq += `Proxy-Authorization: Basic ${creds}\r\n`;
205
+ }
206
+ connectReq += "\r\n";
207
+
208
+ rawSocket.write(connectReq);
209
+
210
+ let responseData = "";
211
+ const onData = (chunk: Buffer) => {
212
+ responseData += chunk.toString("binary");
213
+ if (responseData.includes("\r\n\r\n")) {
214
+ rawSocket.off("data", onData);
215
+ rawSocket.off("error", reject);
216
+
217
+ const firstLine = responseData.split("\r\n")[0];
218
+ if (firstLine.includes(" 200 ")) {
219
+ const tlsSocket = tls.connect({
220
+ socket: rawSocket,
221
+ servername: targetHost,
222
+ ALPNProtocols: ["h2"],
223
+ });
224
+
225
+ tlsSocket.once("secureConnect", () => {
226
+ resolve(tlsSocket);
227
+ });
228
+ tlsSocket.once("error", reject);
229
+ } else {
230
+ rawSocket.destroy();
231
+ reject(new Error(`Proxy tunnel failed: ${firstLine}`));
232
+ }
233
+ }
234
+ };
235
+ rawSocket.on("data", onData);
236
+ });
237
+
238
+ return promise;
239
+ }