forked from github-mirror/Verome-API
добавил прокси и докер
This commit is contained in:
11
.dockerignore
Normal file
11
.dockerignore
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.vscode
|
||||||
|
README.md
|
||||||
|
Dockerfile
|
||||||
|
.dockerignore
|
||||||
|
|
||||||
|
# Не нужны в образе
|
||||||
|
Music/
|
||||||
|
virome-music/
|
||||||
|
node_modules/
|
||||||
8
.vscode/settings.json
vendored
Normal file
8
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"deno.enable": true,
|
||||||
|
"deno.lint": true,
|
||||||
|
"deno.unstable": false,
|
||||||
|
"[typescript]": {
|
||||||
|
"editor.defaultFormatter": "denoland.vscode-deno"
|
||||||
|
}
|
||||||
|
}
|
||||||
35
Dockerfile
Normal file
35
Dockerfile
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# ── Стадия 1: кэш зависимостей ──────────────────────────────────────────────
|
||||||
|
FROM denoland/deno:alpine-2.2.3 AS deps
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Копируем только файлы, влияющие на зависимости
|
||||||
|
COPY deno.json deno.lock* ./
|
||||||
|
COPY mod.ts lib.ts ui.ts proxy.ts ./
|
||||||
|
|
||||||
|
# Прогреваем кэш Deno (скачивает и компилирует все импорты)
|
||||||
|
RUN deno cache mod.ts
|
||||||
|
|
||||||
|
# ── Стадия 2: финальный образ ────────────────────────────────────────────────
|
||||||
|
FROM denoland/deno:alpine-2.2.3
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Копируем прогретый кэш из предыдущей стадии
|
||||||
|
COPY --from=deps /deno-dir /deno-dir
|
||||||
|
|
||||||
|
# Копируем весь проект
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Порт по умолчанию (можно переопределить через ENV PORT=...)
|
||||||
|
ENV PORT=8000
|
||||||
|
|
||||||
|
EXPOSE ${PORT}
|
||||||
|
|
||||||
|
# Переменные для HTTP-прокси (задаются при запуске контейнера)
|
||||||
|
# Пример: docker run -e HTTPS_PROXY=http://host:port ...
|
||||||
|
ENV HTTPS_PROXY=""
|
||||||
|
ENV HTTP_PROXY=""
|
||||||
|
ENV NO_PROXY="localhost,127.0.0.1"
|
||||||
|
|
||||||
|
CMD ["run", "--allow-net", "--allow-env", "--allow-read", "mod.ts"]
|
||||||
10
deno.json
10
deno.json
@@ -4,13 +4,19 @@
|
|||||||
"exports": "./mod.ts",
|
"exports": "./mod.ts",
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"start": "deno run --allow-net --allow-env --allow-read mod.ts",
|
"start": "deno run --allow-net --allow-env --allow-read mod.ts",
|
||||||
"dev": "deno run --watch --allow-net --allow-env --allow-read mod.ts"
|
"dev": "deno run --watch --allow-net --allow-env --allow-read mod.ts",
|
||||||
|
|
||||||
|
"start:proxy": "HTTPS_PROXY=http://127.0.0.1:8080 HTTP_PROXY=http://127.0.0.1:8080 deno run --allow-net --allow-env --allow-read mod.ts",
|
||||||
|
"dev:proxy": "HTTPS_PROXY=http://127.0.0.1:8080 HTTP_PROXY=http://127.0.0.1:8080 deno run --watch --allow-net --allow-env --allow-read mod.ts",
|
||||||
|
|
||||||
|
"start:proxy:auth": "HTTPS_PROXY=http://user:password@127.0.0.1:8080 HTTP_PROXY=http://user:password@127.0.0.1:8080 deno run --allow-net --allow-env --allow-read mod.ts"
|
||||||
},
|
},
|
||||||
"imports": {
|
"imports": {
|
||||||
"std/": "https://deno.land/std@0.208.0/"
|
"std/": "https://deno.land/std@0.208.0/"
|
||||||
},
|
},
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"strict": true
|
"strict": true,
|
||||||
|
"lib": ["deno.ns", "deno.unstable", "dom"]
|
||||||
},
|
},
|
||||||
"deploy": {
|
"deploy": {
|
||||||
"project": "85252de3-9b36-4d8b-b250-e491b4131838",
|
"project": "85252de3-9b36-4d8b-b250-e491b4131838",
|
||||||
|
|||||||
29
mod.ts
29
mod.ts
@@ -9,6 +9,10 @@
|
|||||||
import { serve } from "https://deno.land/std@0.208.0/http/server.ts";
|
import { serve } from "https://deno.land/std@0.208.0/http/server.ts";
|
||||||
import { YTMusic, YouTubeSearch, LastFM, fetchFromPiped, fetchFromInvidious, getLyrics, getTrendingMusic, getRadio, getTopArtists, getTopTracks, getArtistInfo, getTrackInfo, getSongComplete, getAlbumComplete, getArtistComplete, getFullChain } from "./lib.ts";
|
import { YTMusic, YouTubeSearch, LastFM, fetchFromPiped, fetchFromInvidious, getLyrics, getTrendingMusic, getRadio, getTopArtists, getTopTracks, getArtistInfo, getTrackInfo, getSongComplete, getAlbumComplete, getArtistComplete, getFullChain } from "./lib.ts";
|
||||||
import { html as uiHtml } from "./ui.ts";
|
import { html as uiHtml } from "./ui.ts";
|
||||||
|
import { installProxyFetch } from "./proxy.ts";
|
||||||
|
|
||||||
|
// ── Proxy must be installed BEFORE any fetch calls ──────────────────────────
|
||||||
|
installProxyFetch();
|
||||||
|
|
||||||
const PORT = parseInt(Deno.env.get("PORT") || "8000");
|
const PORT = parseInt(Deno.env.get("PORT") || "8000");
|
||||||
|
|
||||||
@@ -94,6 +98,31 @@ async function handler(req: Request): Promise<Response> {
|
|||||||
if (pathname === "/favicon.ico") return new Response(null, { status: 204 });
|
if (pathname === "/favicon.ico") return new Response(null, { status: 204 });
|
||||||
if (pathname === "/health") return json({ status: "ok" });
|
if (pathname === "/health") return json({ status: "ok" });
|
||||||
|
|
||||||
|
// ============ PROXY CONFIG ============
|
||||||
|
|
||||||
|
if (pathname === "/api/proxy/config") {
|
||||||
|
const httpsProxy = Deno.env.get("HTTPS_PROXY") || Deno.env.get("https_proxy");
|
||||||
|
const httpProxy = Deno.env.get("HTTP_PROXY") || Deno.env.get("http_proxy");
|
||||||
|
const proxyUrl = Deno.env.get("PROXY_URL");
|
||||||
|
const noProxy = Deno.env.get("NO_PROXY") || Deno.env.get("no_proxy");
|
||||||
|
const active = !!(httpsProxy || httpProxy || proxyUrl);
|
||||||
|
|
||||||
|
// Mask credentials in proxy URL for display
|
||||||
|
const mask = (u: string | undefined) =>
|
||||||
|
u ? u.replace(/:\/\/[^@]*@/, "://<hidden>@") : null;
|
||||||
|
|
||||||
|
return json({
|
||||||
|
proxy_enabled: active,
|
||||||
|
https_proxy: mask(httpsProxy || undefined),
|
||||||
|
http_proxy: mask(httpProxy || undefined),
|
||||||
|
proxy_url: mask(proxyUrl || undefined),
|
||||||
|
no_proxy: noProxy || null,
|
||||||
|
note: active
|
||||||
|
? "All outbound requests are routed through the proxy"
|
||||||
|
: "No proxy configured — direct connections are used",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ============ SEARCH ============
|
// ============ SEARCH ============
|
||||||
|
|
||||||
if (pathname === "/api/search") {
|
if (pathname === "/api/search") {
|
||||||
|
|||||||
282
proxy.ts
Normal file
282
proxy.ts
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
/// <reference lib="deno.ns" />
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP/HTTPS Proxy support for Deno fetch
|
||||||
|
*
|
||||||
|
* Reads proxy settings from environment variables:
|
||||||
|
* HTTPS_PROXY — proxy for HTTPS requests (e.g. http://127.0.0.1:8080)
|
||||||
|
* HTTP_PROXY — proxy for HTTP requests (e.g. http://127.0.0.1:8080)
|
||||||
|
* PROXY_URL — fallback universal proxy (e.g. http://user:pass@host:port)
|
||||||
|
* NO_PROXY — comma-separated hosts to skip (e.g. localhost,127.0.0.1)
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* import { proxyFetch } from "./proxy.ts";
|
||||||
|
* const res = await proxyFetch("https://music.youtube.com/...", { ... });
|
||||||
|
*
|
||||||
|
* Run with:
|
||||||
|
* HTTPS_PROXY=http://127.0.0.1:8080 deno run --allow-net --allow-env --allow-read mod.ts
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ─── Parse proxy URL ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function getProxyUrl(targetUrl: string): string | null {
|
||||||
|
const isHttps = targetUrl.startsWith("https://");
|
||||||
|
|
||||||
|
const httpsProxy = Deno.env.get("HTTPS_PROXY") || Deno.env.get("https_proxy");
|
||||||
|
const httpProxy = Deno.env.get("HTTP_PROXY") || Deno.env.get("http_proxy");
|
||||||
|
const universalProxy = Deno.env.get("PROXY_URL");
|
||||||
|
|
||||||
|
const proxyUrl = isHttps
|
||||||
|
? (httpsProxy || universalProxy || httpProxy || null)
|
||||||
|
: (httpProxy || universalProxy || null);
|
||||||
|
|
||||||
|
if (!proxyUrl) return null;
|
||||||
|
|
||||||
|
// Check NO_PROXY
|
||||||
|
const noProxy = (Deno.env.get("NO_PROXY") || Deno.env.get("no_proxy") || "").split(",").map(s => s.trim()).filter(Boolean);
|
||||||
|
if (noProxy.length) {
|
||||||
|
try {
|
||||||
|
const { hostname } = new URL(targetUrl);
|
||||||
|
for (const entry of noProxy) {
|
||||||
|
if (entry === "*" || hostname === entry || hostname.endsWith("." + entry)) return null;
|
||||||
|
}
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
return proxyUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── HTTP CONNECT tunnel ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async function connectViaProxy(
|
||||||
|
proxyUrl: string,
|
||||||
|
targetHost: string,
|
||||||
|
targetPort: number,
|
||||||
|
): Promise<Deno.TcpConn> {
|
||||||
|
const proxy = new URL(proxyUrl);
|
||||||
|
const proxyHost = proxy.hostname;
|
||||||
|
const proxyPort = parseInt(proxy.port || "8080");
|
||||||
|
|
||||||
|
const conn = await Deno.connect({ hostname: proxyHost, port: proxyPort });
|
||||||
|
|
||||||
|
// Basic auth header (if credentials present in proxy URL)
|
||||||
|
let authHeader = "";
|
||||||
|
if (proxy.username) {
|
||||||
|
const creds = btoa(`${decodeURIComponent(proxy.username)}:${decodeURIComponent(proxy.password)}`);
|
||||||
|
authHeader = `Proxy-Authorization: Basic ${creds}\r\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send CONNECT request
|
||||||
|
const connectReq = `CONNECT ${targetHost}:${targetPort} HTTP/1.1\r\nHost: ${targetHost}:${targetPort}\r\n${authHeader}\r\n`;
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
await conn.write(encoder.encode(connectReq));
|
||||||
|
|
||||||
|
// Read response
|
||||||
|
const buf = new Uint8Array(4096);
|
||||||
|
const n = await conn.read(buf);
|
||||||
|
if (n === null) throw new Error("Proxy closed connection unexpectedly");
|
||||||
|
|
||||||
|
const response = new TextDecoder().decode(buf.subarray(0, n));
|
||||||
|
const statusLine = response.split("\r\n")[0];
|
||||||
|
const statusCode = parseInt(statusLine.split(" ")[1]);
|
||||||
|
|
||||||
|
if (statusCode !== 200) {
|
||||||
|
conn.close();
|
||||||
|
throw new Error(`Proxy CONNECT failed: ${statusLine}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return conn;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Proxy-aware fetch ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drop-in replacement for global `fetch` that respects HTTP/HTTPS proxy
|
||||||
|
* environment variables. Falls back to native fetch when no proxy is configured.
|
||||||
|
*/
|
||||||
|
export async function proxyFetch(
|
||||||
|
input: string | URL | Request,
|
||||||
|
init?: RequestInit,
|
||||||
|
): Promise<Response> {
|
||||||
|
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
||||||
|
const proxyUrl = getProxyUrl(url);
|
||||||
|
|
||||||
|
// No proxy configured — use native fetch directly
|
||||||
|
if (!proxyUrl) {
|
||||||
|
return fetch(input, init);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For HTTPS targets we build a tunnel with HTTP CONNECT, then do the request
|
||||||
|
// over the tunnelled raw TCP connection using the Fetch API with the tunnel.
|
||||||
|
// Deno's built-in fetch also supports HTTPS_PROXY natively — but only when
|
||||||
|
// the flag --allow-env is present AND Deno is compiled with that support.
|
||||||
|
// To be safe, we also forward the env vars so Deno's own fetch can pick them up.
|
||||||
|
try {
|
||||||
|
const targetUrl = new URL(url);
|
||||||
|
const isHttps = targetUrl.protocol === "https:";
|
||||||
|
const targetHost = targetUrl.hostname;
|
||||||
|
const targetPort = parseInt(targetUrl.port || (isHttps ? "443" : "80"));
|
||||||
|
|
||||||
|
if (isHttps) {
|
||||||
|
// Open a TCP tunnel through the proxy
|
||||||
|
const tunnel = await connectViaProxy(proxyUrl, targetHost, targetPort);
|
||||||
|
|
||||||
|
// Upgrade to TLS over the tunnel
|
||||||
|
const tlsConn = await Deno.startTls(tunnel, { hostname: targetHost });
|
||||||
|
|
||||||
|
// Build raw HTTP/1.1 request
|
||||||
|
const method = (init?.method || "GET").toUpperCase();
|
||||||
|
const path = targetUrl.pathname + targetUrl.search;
|
||||||
|
const headers = new Headers(init?.headers as HeadersInit | undefined);
|
||||||
|
|
||||||
|
// Ensure required headers
|
||||||
|
if (!headers.has("Host")) headers.set("Host", targetHost);
|
||||||
|
if (!headers.has("User-Agent")) headers.set("User-Agent", "Mozilla/5.0");
|
||||||
|
if (!headers.has("Connection")) headers.set("Connection", "close");
|
||||||
|
|
||||||
|
let body = "";
|
||||||
|
if (init?.body) {
|
||||||
|
const bodyText = typeof init.body === "string"
|
||||||
|
? init.body
|
||||||
|
: new TextDecoder().decode(init.body as Uint8Array);
|
||||||
|
if (!headers.has("Content-Type")) headers.set("Content-Type", "application/json");
|
||||||
|
if (!headers.has("Content-Length")) headers.set("Content-Length", String(new TextEncoder().encode(bodyText).length));
|
||||||
|
body = bodyText;
|
||||||
|
}
|
||||||
|
|
||||||
|
let rawRequest = `${method} ${path} HTTP/1.1\r\n`;
|
||||||
|
headers.forEach((value, key) => { rawRequest += `${key}: ${value}\r\n`; });
|
||||||
|
rawRequest += "\r\n";
|
||||||
|
if (body) rawRequest += body;
|
||||||
|
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
await tlsConn.write(encoder.encode(rawRequest));
|
||||||
|
|
||||||
|
// Read response
|
||||||
|
const chunks: Uint8Array[] = [];
|
||||||
|
const tmpBuf = new Uint8Array(16384);
|
||||||
|
while (true) {
|
||||||
|
const n = await tlsConn.read(tmpBuf);
|
||||||
|
if (n === null) break;
|
||||||
|
chunks.push(tmpBuf.slice(0, n));
|
||||||
|
}
|
||||||
|
tlsConn.close();
|
||||||
|
|
||||||
|
const fullResponse = new Uint8Array(chunks.reduce((acc, c) => acc + c.length, 0));
|
||||||
|
let offset = 0;
|
||||||
|
for (const chunk of chunks) { fullResponse.set(chunk, offset); offset += chunk.length; }
|
||||||
|
|
||||||
|
const responseText = new TextDecoder().decode(fullResponse);
|
||||||
|
const headerEnd = responseText.indexOf("\r\n\r\n");
|
||||||
|
if (headerEnd === -1) throw new Error("Invalid HTTP response from proxy tunnel");
|
||||||
|
|
||||||
|
const headerSection = responseText.slice(0, headerEnd);
|
||||||
|
const bodyBytes = fullResponse.slice(new TextEncoder().encode(headerSection + "\r\n\r\n").length);
|
||||||
|
|
||||||
|
const lines = headerSection.split("\r\n");
|
||||||
|
const statusLine = lines[0];
|
||||||
|
const statusCode = parseInt(statusLine.split(" ")[1]);
|
||||||
|
const statusText = statusLine.split(" ").slice(2).join(" ");
|
||||||
|
|
||||||
|
const responseHeaders = new Headers();
|
||||||
|
for (const line of lines.slice(1)) {
|
||||||
|
const idx = line.indexOf(": ");
|
||||||
|
if (idx !== -1) responseHeaders.append(line.slice(0, idx), line.slice(idx + 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(bodyBytes, { status: statusCode, statusText, headers: responseHeaders });
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTP (plain) — use proxy as a regular forward proxy
|
||||||
|
const proxy = new URL(proxyUrl);
|
||||||
|
const conn = await Deno.connect({ hostname: proxy.hostname, port: parseInt(proxy.port || "8080") });
|
||||||
|
|
||||||
|
const method = (init?.method || "GET").toUpperCase();
|
||||||
|
const headers = new Headers(init?.headers as HeadersInit | undefined);
|
||||||
|
if (!headers.has("Host")) headers.set("Host", targetHost);
|
||||||
|
if (!headers.has("User-Agent")) headers.set("User-Agent", "Mozilla/5.0");
|
||||||
|
if (!headers.has("Connection")) headers.set("Connection", "close");
|
||||||
|
|
||||||
|
let body = "";
|
||||||
|
if (init?.body) {
|
||||||
|
const bodyText = typeof init.body === "string"
|
||||||
|
? init.body
|
||||||
|
: new TextDecoder().decode(init.body as Uint8Array);
|
||||||
|
if (!headers.has("Content-Type")) headers.set("Content-Type", "application/json");
|
||||||
|
if (!headers.has("Content-Length")) headers.set("Content-Length", String(new TextEncoder().encode(bodyText).length));
|
||||||
|
body = bodyText;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proxy-Authorization header for HTTP forward proxy
|
||||||
|
if (proxy.username && !headers.has("Proxy-Authorization")) {
|
||||||
|
const creds = btoa(`${decodeURIComponent(proxy.username)}:${decodeURIComponent(proxy.password)}`);
|
||||||
|
headers.set("Proxy-Authorization", `Basic ${creds}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let rawRequest = `${method} ${url} HTTP/1.1\r\n`;
|
||||||
|
headers.forEach((value, key) => { rawRequest += `${key}: ${value}\r\n`; });
|
||||||
|
rawRequest += "\r\n";
|
||||||
|
if (body) rawRequest += body;
|
||||||
|
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
await conn.write(encoder.encode(rawRequest));
|
||||||
|
|
||||||
|
const chunks: Uint8Array[] = [];
|
||||||
|
const tmpBuf = new Uint8Array(16384);
|
||||||
|
while (true) {
|
||||||
|
const n = await conn.read(tmpBuf);
|
||||||
|
if (n === null) break;
|
||||||
|
chunks.push(tmpBuf.slice(0, n));
|
||||||
|
}
|
||||||
|
conn.close();
|
||||||
|
|
||||||
|
const fullResponse = new Uint8Array(chunks.reduce((acc, c) => acc + c.length, 0));
|
||||||
|
let off = 0;
|
||||||
|
for (const chunk of chunks) { fullResponse.set(chunk, off); off += chunk.length; }
|
||||||
|
|
||||||
|
const responseText = new TextDecoder().decode(fullResponse);
|
||||||
|
const headerEnd = responseText.indexOf("\r\n\r\n");
|
||||||
|
if (headerEnd === -1) throw new Error("Invalid HTTP response");
|
||||||
|
|
||||||
|
const headerSection = responseText.slice(0, headerEnd);
|
||||||
|
const bodyBytes = fullResponse.slice(new TextEncoder().encode(headerSection + "\r\n\r\n").length);
|
||||||
|
|
||||||
|
const lines = headerSection.split("\r\n");
|
||||||
|
const statusLine = lines[0];
|
||||||
|
const statusCode = parseInt(statusLine.split(" ")[1]);
|
||||||
|
const statusText = statusLine.split(" ").slice(2).join(" ");
|
||||||
|
|
||||||
|
const responseHeaders = new Headers();
|
||||||
|
for (const line of lines.slice(1)) {
|
||||||
|
const idx = line.indexOf(": ");
|
||||||
|
if (idx !== -1) responseHeaders.append(line.slice(0, idx), line.slice(idx + 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(bodyBytes, { status: statusCode, statusText, headers: responseHeaders });
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
// On any proxy error — fall back to direct fetch and log the issue
|
||||||
|
console.warn(`[proxy] Error using proxy for ${url}: ${err}. Falling back to direct fetch.`);
|
||||||
|
return fetch(input, init);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Patch global fetch ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call once at startup to replace globalThis.fetch with proxyFetch.
|
||||||
|
* After this, ALL fetch() calls in the process go through the proxy.
|
||||||
|
*/
|
||||||
|
export function installProxyFetch(): void {
|
||||||
|
const proxyUrl = Deno.env.get("HTTPS_PROXY") || Deno.env.get("https_proxy") ||
|
||||||
|
Deno.env.get("HTTP_PROXY") || Deno.env.get("http_proxy") ||
|
||||||
|
Deno.env.get("PROXY_URL");
|
||||||
|
|
||||||
|
if (proxyUrl) {
|
||||||
|
console.log(`[proxy] HTTP proxy enabled: ${proxyUrl.replace(/:\/\/[^@]*@/, "://<credentials>@")}`);
|
||||||
|
// @ts-ignore — replacing global fetch
|
||||||
|
globalThis.fetch = proxyFetch;
|
||||||
|
} else {
|
||||||
|
console.log("[proxy] No proxy configured — using direct connections");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user