@arcjet/transport
Advanced tools
| import type { Transport } from "@connectrpc/connect"; | ||
| export type { ProxyEnvironment, TransportLogger, TransportOptions, } from "./detect-proxy.js"; | ||
| import type { TransportOptions } from "./detect-proxy.js"; | ||
| export declare function createTransport(baseUrl: string, options?: TransportOptions): Transport; |
+16
| import { createConnectTransport } from '@connectrpc/connect-web'; | ||
| import { detectProxy } from './detect-proxy.js'; | ||
| function createTransport(baseUrl, options) { | ||
| // Deno's `fetch` performs the proxying itself; we detect to log a line. | ||
| detectProxy(new URL(baseUrl), options); | ||
| return createConnectTransport({ | ||
| baseUrl, | ||
| fetch: fetchProxy, | ||
| }); | ||
| } | ||
| function fetchProxy(input, init) { | ||
| return fetch(input, { ...init, redirect: "follow" }); | ||
| } | ||
| export { createTransport }; |
| /** | ||
| * Map of environment variables used to detect an outbound proxy. | ||
| * | ||
| * This is the same shape as `process.env`. | ||
| */ | ||
| export type ProxyEnvironment = Record<string, string | undefined>; | ||
| /** | ||
| * Minimal logger used to print a line when a proxy is detected. | ||
| */ | ||
| export interface TransportLogger { | ||
| /** | ||
| * Log an informational message. | ||
| * | ||
| * @param message | ||
| * Template. | ||
| * @param interpolationValues | ||
| * Parameters to interpolate. | ||
| * @returns | ||
| * Nothing. | ||
| */ | ||
| info(message: string, ...interpolationValues: unknown[]): void; | ||
| } | ||
| /** | ||
| * Configuration shared by all transports. | ||
| */ | ||
| export interface TransportOptions { | ||
| /** | ||
| * Logger used to print a line at startup when a proxy is detected (optional). | ||
| * | ||
| * Defaults to a logger configured from the `ARCJET_LOG_LEVEL` environment | ||
| * variable. | ||
| */ | ||
| log?: TransportLogger | undefined; | ||
| /** | ||
| * Environment variables used to detect an outbound proxy (optional). | ||
| * | ||
| * Defaults to `process.env` so standard proxy environment variables | ||
| * (`HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY`) are auto-detected. Pass | ||
| * `false` to ignore proxy environment variables entirely. | ||
| */ | ||
| proxyEnv?: ProxyEnvironment | false | undefined; | ||
| /** | ||
| * HTTP version to use when a proxy is in use, on Node.js (optional). | ||
| * | ||
| * Has no effect when no proxy applies, and no effect on Bun, Deno, or the | ||
| * edge runtimes (which proxy through their `fetch` instead). Ignored for | ||
| * direct connections, which always use HTTP/2. | ||
| * | ||
| * - `"1.1"` (default) routes through the proxy over HTTP/1.1 using the | ||
| * built-in proxy support of the Node.js HTTP agent. This works with any | ||
| * proxy the agent supports, but loses the latency benefits of HTTP/2. | ||
| * - `"2"` establishes an HTTP `CONNECT` tunnel and keeps HTTP/2 to the origin | ||
| * end-to-end. This requires a tunneling (`CONNECT`) proxy — the common kind | ||
| * for HTTPS egress — and a proxy that does not buffer the tunnel (see the | ||
| * proxy support notes in the README). A proxy that terminates TLS and | ||
| * speaks HTTP/1.1 to origins cannot preserve HTTP/2 regardless. | ||
| * | ||
| * Defaults to `"1.1"`. | ||
| */ | ||
| proxyHttpVersion?: "1.1" | "2" | undefined; | ||
| } | ||
| /** | ||
| * Detect the proxy that applies to a URL and log a line when one is found. | ||
| * | ||
| * Standard proxy environment variables (`HTTP_PROXY` and `HTTPS_PROXY`, | ||
| * respecting `NO_PROXY`) are auto-detected. When a proxy applies, a single line | ||
| * is logged at startup so it is easy to know when a proxy is being used. The | ||
| * proxy URL itself is not logged, since it can contain credentials. | ||
| * | ||
| * Takes an already-parsed `URL` so callers that also need it (e.g. to pick an | ||
| * HTTP vs HTTPS agent) don't parse the base URL twice. | ||
| * | ||
| * @param url | ||
| * URL that requests will be made to. | ||
| * @param options | ||
| * Configuration (optional). | ||
| * @returns | ||
| * Proxy URL that applies to `url`, or `undefined` when no proxy applies. | ||
| */ | ||
| export declare function detectProxy(url: URL, options?: TransportOptions): string | undefined; |
+213
| import process from 'node:process'; | ||
| import { logLevel } from '@arcjet/env'; | ||
| import { Logger } from '@arcjet/logger'; | ||
| /** | ||
| * Detect the proxy that applies to a URL and log a line when one is found. | ||
| * | ||
| * Standard proxy environment variables (`HTTP_PROXY` and `HTTPS_PROXY`, | ||
| * respecting `NO_PROXY`) are auto-detected. When a proxy applies, a single line | ||
| * is logged at startup so it is easy to know when a proxy is being used. The | ||
| * proxy URL itself is not logged, since it can contain credentials. | ||
| * | ||
| * Takes an already-parsed `URL` so callers that also need it (e.g. to pick an | ||
| * HTTP vs HTTPS agent) don't parse the base URL twice. | ||
| * | ||
| * @param url | ||
| * URL that requests will be made to. | ||
| * @param options | ||
| * Configuration (optional). | ||
| * @returns | ||
| * Proxy URL that applies to `url`, or `undefined` when no proxy applies. | ||
| */ | ||
| function detectProxy(url, options) { | ||
| // Default to detecting proxy configuration from `process.env`. Passing | ||
| // `false` disables proxy detection entirely. | ||
| const proxyEnv = options?.proxyEnv === false | ||
| ? undefined | ||
| : (options?.proxyEnv ?? process.env); | ||
| let proxyUrl; | ||
| try { | ||
| proxyUrl = proxyEnv ? proxyForUrl(url, proxyEnv) : undefined; | ||
| } | ||
| catch { | ||
| // Reading proxy environment variables can throw on runtimes that gate | ||
| // environment access behind a permission (e.g. Deno without `--allow-env`). | ||
| // Treat that as "no proxy" rather than failing transport creation. | ||
| return undefined; | ||
| } | ||
| if (typeof proxyUrl === "string") { | ||
| // Log a line at startup so it is easy to know when a proxy is being used. | ||
| // We deliberately do not log the proxy URL itself: it can contain | ||
| // credentials, and not logging it is simpler and safer than redacting it. | ||
| let log = options?.log; | ||
| if (!log) { | ||
| try { | ||
| log = new Logger({ | ||
| level: logLevel({ ARCJET_LOG_LEVEL: process.env.ARCJET_LOG_LEVEL }), | ||
| }); | ||
| } | ||
| catch { | ||
| // Building the default logger reads `ARCJET_LOG_LEVEL`, which can throw | ||
| // on runtimes that gate environment access (e.g. Deno without | ||
| // `--allow-env`) when the proxy came from an explicit `proxyEnv`. Skip | ||
| // the startup line rather than failing transport creation; the proxy is | ||
| // still returned below. | ||
| } | ||
| } | ||
| log?.info("Connecting to the Arcjet API through a proxy"); | ||
| } | ||
| return proxyUrl; | ||
| } | ||
| // --------------------------------------------------------------------------- | ||
| // Keep the proxy-resolution logic below in sync with the copy in | ||
| // `@arcjet/guard` (`arcjet-guard/src/detect-proxy.ts`). The two packages | ||
| // intentionally duplicate it rather than share a module: `@arcjet/guard` | ||
| // bundles a fetch transport that runs on edge runtimes without `process` or | ||
| // these dependencies, so it keeps an edge-safe copy with no imports. Only the | ||
| // `detectProxy` entry point above differs between the copies; the helpers | ||
| // below should stay logically identical (the two may differ only in line | ||
| // wrapping, since each package runs a different formatter). | ||
| // --------------------------------------------------------------------------- | ||
| /** | ||
| * Find the proxy that should be used for a URL, if any. | ||
| * | ||
| * Honors `NO_PROXY` so the result reflects the connection that will actually be | ||
| * made. | ||
| * | ||
| * @param url | ||
| * URL that requests will be made to. | ||
| * @param proxyEnv | ||
| * Environment variables to inspect. | ||
| * @returns | ||
| * Proxy URL to use, or `undefined` when no proxy applies. | ||
| */ | ||
| function proxyForUrl(url, proxyEnv) { | ||
| // httpoxy mitigation: under CGI the inbound `Proxy` request header is exposed | ||
| // as the `HTTP_PROXY` environment variable, so honoring uppercase `HTTP_PROXY` | ||
| // for HTTP targets could let a request control outbound proxying. When a CGI | ||
| // environment is detected (`REQUEST_METHOD` is set), ignore it and use only | ||
| // the lowercase `http_proxy`. See https://httpoxy.org. | ||
| const httpProxy = proxyEnv["REQUEST_METHOD"] === undefined | ||
| ? firstValue(proxyEnv["http_proxy"], proxyEnv["HTTP_PROXY"]) | ||
| : firstValue(proxyEnv["http_proxy"]); | ||
| const proxyUrl = url.protocol === "https:" | ||
| ? firstValue(proxyEnv["https_proxy"], proxyEnv["HTTPS_PROXY"]) | ||
| : httpProxy; | ||
| if (typeof proxyUrl !== "string") { | ||
| return undefined; | ||
| } | ||
| if (isNoProxy(url, firstValue(proxyEnv["no_proxy"], proxyEnv["NO_PROXY"]))) { | ||
| return undefined; | ||
| } | ||
| return proxyUrl; | ||
| } | ||
| /** | ||
| * Determine whether a URL should bypass the proxy because of `NO_PROXY`. | ||
| * | ||
| * Supports the common `NO_PROXY` syntax: a comma- or space-separated list of | ||
| * host suffixes, an optional leading `.` or `*.`, an optional `:port`, and `*` | ||
| * to match everything. Entries are matched as host names; IP/CIDR ranges (e.g. | ||
| * `10.0.0.0/8`) are not supported, the same as curl. | ||
| * | ||
| * @param url | ||
| * URL that requests will be made to. | ||
| * @param noProxy | ||
| * Value of the `NO_PROXY` environment variable. | ||
| * @returns | ||
| * Whether the proxy should be bypassed. | ||
| */ | ||
| function isNoProxy(url, noProxy) { | ||
| if (typeof noProxy !== "string") { | ||
| return false; | ||
| } | ||
| // `url.hostname` wraps IPv6 addresses in brackets (e.g. `[::1]`); strip them | ||
| // so entries can be written with or without brackets. | ||
| const hostname = url.hostname.toLowerCase().replaceAll(/^\[|\]$/g, ""); | ||
| const port = url.port === "" ? (url.protocol === "https:" ? "443" : "80") : url.port; | ||
| for (const raw of noProxy.split(/[\s,]+/)) { | ||
| if (raw === "") { | ||
| continue; | ||
| } | ||
| // `*` bypasses the proxy for every host. | ||
| if (raw === "*") { | ||
| return true; | ||
| } | ||
| const entry = parseNoProxyEntry(raw); | ||
| // A port on the entry must match the target's (default) port. | ||
| if (entry.port !== undefined && entry.port !== port) { | ||
| continue; | ||
| } | ||
| if (entry.host !== "" && hostMatches(hostname, entry.host)) { | ||
| return true; | ||
| } | ||
| } | ||
| return false; | ||
| } | ||
| /** | ||
| * Parse one `NO_PROXY` entry into its host and optional port. | ||
| * | ||
| * @param raw | ||
| * A single entry from the `NO_PROXY` list (already split out and non-empty). | ||
| * @returns | ||
| * The lowercased host (with any `*.`/`.` wildcard prefix and IPv6 brackets | ||
| * removed) and the explicit `:port`, if the entry had one. | ||
| */ | ||
| function parseNoProxyEntry(raw) { | ||
| const entry = raw.toLowerCase(); | ||
| // Split off an optional `:port`. A bracketed IPv6 entry (`[::1]:8080`) keeps | ||
| // its port outside the brackets, a bare IPv6 entry (`::1`) has no port, and | ||
| // everything else treats a single trailing `:<digits>` as the port (so IPv6 | ||
| // colons are not mistaken for one). | ||
| let host = entry; | ||
| let port; | ||
| const bracketed = entry.match(/^\[(.+)\](?::([0-9]+))?$/); | ||
| if (bracketed === null) { | ||
| const colon = entry.lastIndexOf(":"); | ||
| if (colon !== -1 && | ||
| colon === entry.indexOf(":") && | ||
| /^[0-9]+$/.test(entry.slice(colon + 1))) { | ||
| host = entry.slice(0, colon); | ||
| port = entry.slice(colon + 1); | ||
| } | ||
| } | ||
| else { | ||
| host = bracketed[1] ?? ""; | ||
| port = bracketed[2]; | ||
| } | ||
| // Strip a leading `*.` or `.` so `.example.com`, `*.example.com`, and | ||
| // `example.com` all match the domain and its subdomains. | ||
| return { host: host.replace(/^\*?\./, ""), port }; | ||
| } | ||
| /** | ||
| * Whether a host name matches a `NO_PROXY` entry host, exactly or as a | ||
| * subdomain. | ||
| * | ||
| * @param hostname | ||
| * Host name of the URL being requested. | ||
| * @param host | ||
| * Host parsed from a `NO_PROXY` entry. | ||
| * @returns | ||
| * Whether the host name is, or is a subdomain of, the entry host. | ||
| */ | ||
| function hostMatches(hostname, host) { | ||
| return hostname === host || hostname.endsWith("." + host); | ||
| } | ||
| /** | ||
| * Get the first non-empty string from a list of values. | ||
| * | ||
| * @param values | ||
| * Values to inspect. | ||
| * @returns | ||
| * First non-empty string, or `undefined`. | ||
| */ | ||
| function firstValue(...values) { | ||
| for (const value of values) { | ||
| if (typeof value === "string" && value !== "") { | ||
| return value; | ||
| } | ||
| } | ||
| return undefined; | ||
| } | ||
| export { detectProxy }; |
| import type { SecureClientSessionOptions } from "node:http2"; | ||
| import { Duplex } from "node:stream"; | ||
| /** | ||
| * Route an HTTP/2 session through a forward proxy using an HTTP `CONNECT` | ||
| * tunnel, preserving HTTP/2 to the origin. | ||
| * | ||
| * Node's built-in HTTP agent proxy support (and the `https-proxy-agent` family) | ||
| * only wire a proxy into the HTTP/1.1 agent, which is why proxying otherwise | ||
| * forces a downgrade from HTTP/2. But HTTP/2 survives a `CONNECT` tunnel | ||
| * end-to-end: the proxy is told to open a raw TCP tunnel and thereafter only | ||
| * blindly forwards bytes (RFC 9110 §9.3.6), so the TLS handshake — including the | ||
| * ALPN negotiation that selects `h2` — happens directly with the origin. The | ||
| * proxy never sees, and so cannot downgrade, the negotiated protocol. | ||
| * | ||
| * The one wrinkle is that {@linkcode http2.connect}'s `createConnection` | ||
| * callback must return a {@linkcode Duplex} synchronously, but the `CONNECT` | ||
| * handshake is asynchronous. We bridge that gap with a small `Duplex` that | ||
| * buffers whatever the consumer writes (the TLS `ClientHello`, or the HTTP/2 | ||
| * client preface for a cleartext target) until the proxy answers `2xx`, then | ||
| * splices itself onto the proxy socket. Because the contract stays synchronous, | ||
| * this drops into `@connectrpc/connect-node`'s default `Http2SessionManager` | ||
| * via `nodeOptions.createConnection` with no fork — reconnection, pings, and the | ||
| * idle timeout all keep working. | ||
| * | ||
| * This is Node-only. Bun and Deno don't implement the agent option this sits | ||
| * alongside, and their `fetch` is used for proxying instead. | ||
| * | ||
| * @param proxyUrl | ||
| * Proxy to route through (for example `http://127.0.0.1:3128`). An HTTPS proxy | ||
| * (TLS to the proxy itself) is supported too. | ||
| * @returns | ||
| * A `createConnection` callback for `http2.connect(..., { createConnection })` | ||
| * (and therefore for connect-node's `nodeOptions.createConnection`). | ||
| */ | ||
| export declare function createTunnelingConnection(proxyUrl: string): (authority: URL, options: SecureClientSessionOptions) => Duplex; |
+239
| import * as net from 'node:net'; | ||
| import { Duplex } from 'node:stream'; | ||
| import * as tls from 'node:tls'; | ||
| /** | ||
| * Route an HTTP/2 session through a forward proxy using an HTTP `CONNECT` | ||
| * tunnel, preserving HTTP/2 to the origin. | ||
| * | ||
| * Node's built-in HTTP agent proxy support (and the `https-proxy-agent` family) | ||
| * only wire a proxy into the HTTP/1.1 agent, which is why proxying otherwise | ||
| * forces a downgrade from HTTP/2. But HTTP/2 survives a `CONNECT` tunnel | ||
| * end-to-end: the proxy is told to open a raw TCP tunnel and thereafter only | ||
| * blindly forwards bytes (RFC 9110 §9.3.6), so the TLS handshake — including the | ||
| * ALPN negotiation that selects `h2` — happens directly with the origin. The | ||
| * proxy never sees, and so cannot downgrade, the negotiated protocol. | ||
| * | ||
| * The one wrinkle is that {@linkcode http2.connect}'s `createConnection` | ||
| * callback must return a {@linkcode Duplex} synchronously, but the `CONNECT` | ||
| * handshake is asynchronous. We bridge that gap with a small `Duplex` that | ||
| * buffers whatever the consumer writes (the TLS `ClientHello`, or the HTTP/2 | ||
| * client preface for a cleartext target) until the proxy answers `2xx`, then | ||
| * splices itself onto the proxy socket. Because the contract stays synchronous, | ||
| * this drops into `@connectrpc/connect-node`'s default `Http2SessionManager` | ||
| * via `nodeOptions.createConnection` with no fork — reconnection, pings, and the | ||
| * idle timeout all keep working. | ||
| * | ||
| * This is Node-only. Bun and Deno don't implement the agent option this sits | ||
| * alongside, and their `fetch` is used for proxying instead. | ||
| * | ||
| * @param proxyUrl | ||
| * Proxy to route through (for example `http://127.0.0.1:3128`). An HTTPS proxy | ||
| * (TLS to the proxy itself) is supported too. | ||
| * @returns | ||
| * A `createConnection` callback for `http2.connect(..., { createConnection })` | ||
| * (and therefore for connect-node's `nodeOptions.createConnection`). | ||
| */ | ||
| function createTunnelingConnection(proxyUrl) { | ||
| const proxy = new URL(proxyUrl); | ||
| const proxyIsHttps = proxy.protocol === "https:"; | ||
| const proxyPort = Number(proxy.port) || (proxyIsHttps ? 443 : 80); | ||
| // `Proxy-Authorization` header from any credentials embedded in the proxy URL. | ||
| const proxyAuthorization = proxy.username === "" | ||
| ? undefined | ||
| : "Basic " + | ||
| Buffer.from(decodeURIComponent(proxy.username) + | ||
| ":" + | ||
| decodeURIComponent(proxy.password)).toString("base64"); | ||
| return function createConnection(authority, options) { | ||
| const originIsHttps = authority.protocol === "https:"; | ||
| const originPort = Number(authority.port) || (originIsHttps ? 443 : 80); | ||
| const originAuthority = authority.hostname + ":" + originPort; | ||
| // The bridge is the underlying transport the HTTP/2 client writes into. We | ||
| // hold those bytes until the `CONNECT` tunnel is established, then flush and | ||
| // splice the bridge onto the proxy socket. | ||
| let tunnelReady = false; | ||
| const pending = []; | ||
| const bridge = new Duplex({ | ||
| read() { | ||
| // Push-driven; see the splice below. | ||
| }, | ||
| write(chunk, _encoding, callback) { | ||
| const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk); | ||
| if (tunnelReady) { | ||
| proxySocket.write(buffer, callback); | ||
| } | ||
| else { | ||
| pending.push({ chunk: buffer, callback }); | ||
| } | ||
| }, | ||
| }); | ||
| // 1. Open the connection to the proxy itself. Typed as the common | ||
| // `net.Socket` supertype (a `tls.TLSSocket` is one) so `.on(...)` event | ||
| // listeners resolve against a single typed event map rather than a union | ||
| // that would leave their parameters implicitly `any`. | ||
| const proxySocket = proxyIsHttps | ||
| ? tls.connect({ | ||
| host: proxy.hostname, | ||
| port: proxyPort, | ||
| servername: proxy.hostname, | ||
| }) | ||
| : net.connect({ host: proxy.hostname, port: proxyPort }); | ||
| // Disable Nagle's algorithm on the tunnel. HTTP/2 sends many small, | ||
| // dependent control frames; left on, Nagle interacts with the peer's | ||
| // delayed ACK to add ~40ms per round trip. Node sets this on its own HTTP/2 | ||
| // sockets, but since we supply the socket we must set it ourselves. Note | ||
| // that an intermediate proxy that buffers the tunnel can reintroduce the | ||
| // same penalty regardless of this setting. | ||
| proxySocket.setNoDelay(true); | ||
| // 2. Once connected to the proxy, ask it to tunnel to the origin authority. | ||
| proxySocket.once(proxyIsHttps ? "secureConnect" : "connect", () => { | ||
| let request = "CONNECT " + originAuthority + " HTTP/1.1\r\n"; | ||
| request += "Host: " + originAuthority + "\r\n"; | ||
| if (proxyAuthorization !== undefined) { | ||
| request += "Proxy-Authorization: " + proxyAuthorization + "\r\n"; | ||
| } | ||
| request += "\r\n"; | ||
| proxySocket.write(request); | ||
| }); | ||
| // 3. Read the proxy's response. Buffer until the header terminator, check | ||
| // the status line, and only on `2xx` splice the tunnel onto the bridge. | ||
| let head = Buffer.alloc(0); | ||
| function onData(chunk) { | ||
| head = Buffer.concat([head, chunk]); | ||
| const terminator = head.indexOf("\r\n\r\n"); | ||
| if (terminator === -1) { | ||
| return; // Wait for the full response head. | ||
| } | ||
| proxySocket.off("data", onData); | ||
| const statusLine = head | ||
| .subarray(0, head.indexOf("\r\n")) | ||
| .toString("latin1"); | ||
| const status = Number(statusLine.split(" ")[1]); | ||
| if (!(status >= 200 && status < 300)) { | ||
| const error = new Error("Proxy CONNECT failed with status: " + statusLine.trim()); | ||
| proxySocket.destroy(error); | ||
| bridge.destroy(error); | ||
| return; | ||
| } | ||
| // Anything past the header terminator is already tunnel data (rare for a | ||
| // fresh handshake, but never drop it). | ||
| const leftover = head.subarray(terminator + 4); | ||
| if (leftover.length > 0) { | ||
| bridge.push(leftover); | ||
| } | ||
| // Splice: proxy -> bridge, so origin bytes become readable by the client. | ||
| // | ||
| // For a cleartext (h2c) origin the bridge *is* the HTTP/2 socket, so these | ||
| // bytes are HTTP/2 frames. A proxy can coalesce a whole response | ||
| // (HEADERS + DATA + END_STREAM) into one chunk; delivered as a single | ||
| // synchronous read, the HTTP/2 client surfaces the DATA and the stream end | ||
| // in one turn, which races `@connectrpc/connect-node`'s async-iterator | ||
| // response reader and intermittently throws `ERR_STREAM_PREMATURE_CLOSE` | ||
| // on Node >= 26 (a real or TLS-wrapped socket avoids this — it delivers | ||
| // frames across separate reads). So for h2c we split the stream on HTTP/2 | ||
| // frame boundaries and push one frame per tick, preserving order; the | ||
| // client then reads the DATA before observing END_STREAM. For an HTTPS | ||
| // origin the bytes are TLS records (the origin TLS socket reframes them), | ||
| // so we forward them unchanged. | ||
| if (originIsHttps) { | ||
| proxySocket.on("data", (data) => bridge.push(data)); | ||
| } | ||
| else { | ||
| // HTTP/2's default `SETTINGS_MAX_FRAME_SIZE` is 16 KiB and we never raise | ||
| // it, so any frame larger than this generous cap is already a protocol | ||
| // violation. Bounding the buffer keeps a malicious or broken peer from | ||
| // using the 24-bit length field (up to ~16 MiB) to make us buffer an | ||
| // oversized frame before forwarding anything. | ||
| const maxFramePayload = 2 ** 20; // 1 MiB | ||
| let inbound = Buffer.alloc(0); | ||
| proxySocket.on("data", (data) => { | ||
| inbound = Buffer.concat([inbound, data]); | ||
| // An HTTP/2 frame is a 9-byte header (24-bit length, then type, flags, | ||
| // and stream id) followed by `length` bytes of payload. | ||
| while (inbound.length >= 9) { | ||
| const payloadLength = inbound.readUIntBE(0, 3); | ||
| if (payloadLength > maxFramePayload) { | ||
| bridge.destroy(new Error("Proxy tunnel received an oversized HTTP/2 frame")); | ||
| return; | ||
| } | ||
| const frameLength = 9 + payloadLength; | ||
| if (inbound.length < frameLength) { | ||
| break; // Wait for the rest of this frame. | ||
| } | ||
| const frame = inbound.subarray(0, frameLength); | ||
| inbound = inbound.subarray(frameLength); | ||
| setImmediate(() => { | ||
| if (!bridge.destroyed) { | ||
| bridge.push(frame); | ||
| } | ||
| }); | ||
| } | ||
| }); | ||
| } | ||
| proxySocket.on("end", () => setImmediate(() => { | ||
| if (!bridge.destroyed) { | ||
| bridge.push(null); | ||
| } | ||
| })); | ||
| // Flush whatever the client buffered while the tunnel was establishing. | ||
| tunnelReady = true; | ||
| for (const { chunk: queued, callback } of pending) { | ||
| proxySocket.write(queued, callback); | ||
| } | ||
| pending.length = 0; | ||
| // The CONNECT response head is no longer needed; release it so it isn't | ||
| // retained for the lifetime of the (long-lived) connection. | ||
| head = Buffer.alloc(0); | ||
| } | ||
| proxySocket.on("data", onData); | ||
| // Propagate failures in both directions so the session manager observes them. | ||
| proxySocket.on("error", (error) => bridge.destroy(error)); | ||
| bridge.on("close", () => proxySocket.destroy()); | ||
| // 4. For an HTTPS origin, run TLS *to the origin* over the tunnel, offering | ||
| // h2 via ALPN; ALPN is negotiated with the origin, not the proxy. For a | ||
| // cleartext origin, the bridge itself carries HTTP/2 (h2c) over the | ||
| // tunnel. | ||
| if (!originIsHttps) { | ||
| return bridge; | ||
| } | ||
| // `URL.hostname` wraps an IPv6 literal in brackets (e.g. `[::1]`), which | ||
| // `net.isIP` does not recognise. Strip them so an IPv6 origin is detected as | ||
| // an IP (and so the certificate is matched against the bare address — an IP | ||
| // SAN has no brackets). | ||
| const bareHostname = authority.hostname.startsWith("[") && authority.hostname.endsWith("]") | ||
| ? authority.hostname.slice(1, -1) | ||
| : authority.hostname; | ||
| const originIsIpLiteral = net.isIP(bareHostname) !== 0; | ||
| try { | ||
| return tls.connect({ | ||
| ...options, | ||
| socket: bridge, | ||
| // Validate the certificate against the origin host. For a hostname this | ||
| // is also sent as SNI below; for an IP it is the identity to match | ||
| // (since SNI is omitted), so the cert's IP SAN is still checked. | ||
| host: bareHostname, | ||
| // RFC 6066 §3 forbids an IP literal in the TLS SNI `servername` — it must | ||
| // be a DNS hostname — so we omit SNI for an IP origin (IPv4 or IPv6) and | ||
| // rely on `host` above for certificate validation. Node <= 24 tolerated | ||
| // an IP here (deprecation warning); Node >= 26 enforces the RFC and | ||
| // throws `ERR_INVALID_ARG_VALUE`. | ||
| servername: originIsIpLiteral ? undefined : bareHostname, | ||
| ALPNProtocols: ["h2"], | ||
| }); | ||
| } | ||
| catch (error) { | ||
| // Constructing the origin TLS connection can throw *synchronously* (for | ||
| // example, Node >= 26 rejects an IP-literal `servername` per RFC 6066). | ||
| // We have already opened `proxySocket`; if we let the throw escape we | ||
| // strand that open handle, which keeps the event loop — and the whole | ||
| // process — alive (this is what hung CI). Tear both sides down before | ||
| // rethrowing so a failed setup fails fast and cleanly. | ||
| proxySocket.destroy(); | ||
| bridge.destroy(error); | ||
| throw error; | ||
| } | ||
| }; | ||
| } | ||
| export { createTunnelingConnection }; |
+4
-1
@@ -1,1 +0,4 @@ | ||
| export declare function createTransport(baseUrl: string): import("@connectrpc/connect").Transport; | ||
| import type { Transport } from "@connectrpc/connect"; | ||
| export type { ProxyEnvironment, TransportLogger, TransportOptions, } from "./detect-proxy.js"; | ||
| import type { TransportOptions } from "./detect-proxy.js"; | ||
| export declare function createTransport(baseUrl: string, options?: TransportOptions): Transport; |
+4
-4
| import { createConnectTransport } from '@connectrpc/connect-web'; | ||
| import { detectProxy } from './detect-proxy.js'; | ||
| // This file is used when running in Bun. | ||
| // It uses DOM based APIs (`@connectrpc/connect-web`) to connect to the API. | ||
| // Bun slightly differs in how it implements Node APIs and that causes problems. | ||
| function createTransport(baseUrl) { | ||
| function createTransport(baseUrl, options) { | ||
| // Bun's `fetch` performs the proxying itself; we detect to log a line. | ||
| detectProxy(new URL(baseUrl), options); | ||
| return createConnectTransport({ | ||
@@ -8,0 +8,0 @@ baseUrl, |
+4
-1
@@ -1,1 +0,4 @@ | ||
| export declare function createTransport(baseUrl: string): import("@connectrpc/connect").Transport; | ||
| import type { Transport } from "@connectrpc/connect"; | ||
| export type { ProxyEnvironment, TransportLogger, TransportOptions, } from "./detect-proxy.js"; | ||
| import type { TransportOptions } from "./detect-proxy.js"; | ||
| export declare function createTransport(baseUrl: string, _options?: TransportOptions): Transport; |
+5
-15
| import { createConnectTransport } from '@connectrpc/connect-web'; | ||
| // This file is used when running on the `edge-light` condition. | ||
| // Specifically Edge by Vercel. | ||
| // It is the same as `workerd.ts`, which runs on Cloudflare. | ||
| // It uses DOM based APIs (`@connectrpc/connect-web`) to connect to the API. | ||
| // Differing from `bun.ts` this solves the `redirect` option set to `error` | ||
| // inside `connect` as that does not work on the edge. | ||
| // | ||
| // For more information, see: | ||
| // | ||
| // * <https://github.com/connectrpc/connect-es/pull/589> | ||
| // * <https://github.com/connectrpc/connect-es/issues/749#issuecomment-1693507516> | ||
| // * <https://github.com/connectrpc/connect-es/pull/1082> | ||
| // * <https://github.com/e2b-dev/E2B/pull/669/files> | ||
| // * <https://github.com/connectrpc/connect-es/issues/577#issuecomment-2210103503> | ||
| function createTransport(baseUrl) { | ||
| function createTransport(baseUrl, | ||
| // These edge runtimes don't support outbound proxy environment variables, so | ||
| // the options are accepted for API parity with the other entry points but no | ||
| // proxy is detected or used. | ||
| _options) { | ||
| return createConnectTransport({ | ||
@@ -19,0 +9,0 @@ baseUrl, |
+15
-2
@@ -0,11 +1,24 @@ | ||
| import type { Transport } from "@connectrpc/connect"; | ||
| export type { ProxyEnvironment, TransportLogger, TransportOptions, } from "./detect-proxy.js"; | ||
| import type { TransportOptions } from "./detect-proxy.js"; | ||
| /** | ||
| * Create a transport that talks over HTTP/2 using Connect RPC. | ||
| * Create a transport that talks to the Arcjet API using Connect RPC. | ||
| * | ||
| * A thin wrapper around {@linkcode createConnectTransport}. | ||
| * | ||
| * When a standard proxy environment variable (`HTTP_PROXY` or `HTTPS_PROXY`, | ||
| * respecting `NO_PROXY`) is detected, the transport routes requests through the | ||
| * proxy and logs a line at startup. By default it proxies over HTTP/1.1 using | ||
| * the built-in proxy support of the Node.js HTTP agent; set | ||
| * `options.proxyHttpVersion` to `"2"` to instead tunnel HTTP/2 to the origin | ||
| * via `CONNECT` (see {@linkcode TransportOptions.proxyHttpVersion}). Without a | ||
| * proxy it always connects directly over HTTP/2. | ||
| * | ||
| * @param baseUrl | ||
| * Base URI for all HTTP requests (example: `https://example.com/my-api`). | ||
| * @param options | ||
| * Configuration (optional). | ||
| * @returns | ||
| * Connect transport used to make RPC calls. | ||
| */ | ||
| export declare function createTransport(baseUrl: string): import("@connectrpc/connect").Transport; | ||
| export declare function createTransport(baseUrl: string, options?: TransportOptions): Transport; |
+88
-17
@@ -1,30 +0,101 @@ | ||
| import { Http2SessionManager, createConnectTransport } from '@connectrpc/connect-node'; | ||
| import { createConnectTransport, Http2SessionManager } from '@connectrpc/connect-node'; | ||
| import * as http from 'node:http'; | ||
| import * as https from 'node:https'; | ||
| import { detectProxy } from './detect-proxy.js'; | ||
| import { createTunnelingConnection } from './proxy-tunnel.js'; | ||
| /** | ||
| * Create a transport that talks over HTTP/2 using Connect RPC. | ||
| * Create a transport that talks to the Arcjet API using Connect RPC. | ||
| * | ||
| * A thin wrapper around {@linkcode createConnectTransport}. | ||
| * | ||
| * When a standard proxy environment variable (`HTTP_PROXY` or `HTTPS_PROXY`, | ||
| * respecting `NO_PROXY`) is detected, the transport routes requests through the | ||
| * proxy and logs a line at startup. By default it proxies over HTTP/1.1 using | ||
| * the built-in proxy support of the Node.js HTTP agent; set | ||
| * `options.proxyHttpVersion` to `"2"` to instead tunnel HTTP/2 to the origin | ||
| * via `CONNECT` (see {@linkcode TransportOptions.proxyHttpVersion}). Without a | ||
| * proxy it always connects directly over HTTP/2. | ||
| * | ||
| * @param baseUrl | ||
| * Base URI for all HTTP requests (example: `https://example.com/my-api`). | ||
| * @param options | ||
| * Configuration (optional). | ||
| * @returns | ||
| * Connect transport used to make RPC calls. | ||
| */ | ||
| function createTransport(baseUrl) { | ||
| // We create our own session manager so we can attempt to pre-connect | ||
| const sessionManager = new Http2SessionManager(baseUrl, { | ||
| // AWS Global Accelerator doesn't support PING so we use a very high idle | ||
| // timeout. Ref: | ||
| // https://docs.aws.amazon.com/global-accelerator/latest/dg/introduction-how-it-works.html#about-idle-timeout | ||
| idleConnectionTimeoutMs: 340 * 1000, | ||
| }); | ||
| // We ignore the promise result because this is an optimistic pre-connect | ||
| sessionManager.connect(); | ||
| return createConnectTransport({ | ||
| baseUrl, | ||
| httpVersion: "2", | ||
| sessionManager, | ||
| }); | ||
| function createTransport(baseUrl, options) { | ||
| const url = new URL(baseUrl); | ||
| const proxyUrl = detectProxy(url, options); | ||
| if (typeof proxyUrl === "string") { | ||
| if (options?.proxyHttpVersion === "2") { | ||
| // HTTP/2 through the proxy: open a `CONNECT` tunnel and keep HTTP/2 to | ||
| // the origin end-to-end. The proxy only blindly forwards the tunnel, so | ||
| // ALPN is negotiated directly with the origin — see `./proxy-tunnel.ts`. | ||
| return createHttp2Transport(baseUrl, createTunnelingConnection(proxyUrl)); | ||
| } | ||
| // HTTP/1.1 through the proxy (default). Hand the agent only the single | ||
| // proxy variable we resolved (not the whole environment) so it routes | ||
| // through exactly the proxy our detection chose, honoring our `NO_PROXY` | ||
| // handling. `keepAlive` lets the agent reuse the connection to the proxy | ||
| // across requests, like the long-lived session of the direct HTTP/2 path. | ||
| // | ||
| // Type the literal with the exact proxy variable names so a misspelled key | ||
| // is a compile error. The agent's `proxyEnv` option only exists in | ||
| // @types/node 24.x, but this source is also type-checked on the 22.x line | ||
| // (e.g. when bundled into @arcjet/next or @arcjet/sveltekit), so `proxyEnv` | ||
| // is added through an intersection type rather than relying on it being a | ||
| // known `AgentOptions` property. The `as unknown as ProcessEnv` is needed | ||
| // because some augmentations (e.g. Next.js) make `ProcessEnv` require | ||
| // `NODE_ENV`; the object is correct at runtime. | ||
| const isHttps = url.protocol === "https:"; | ||
| const proxyEnvironment = isHttps ? { HTTPS_PROXY: proxyUrl } : { HTTP_PROXY: proxyUrl }; | ||
| const agentOptions = { | ||
| keepAlive: true, | ||
| proxyEnv: proxyEnvironment, | ||
| }; | ||
| const agent = isHttps | ||
| ? new https.Agent(agentOptions) | ||
| : new http.Agent(agentOptions); | ||
| // Node's built-in proxy support only works over HTTP/1.1. | ||
| return createConnectTransport({ | ||
| baseUrl, | ||
| httpVersion: "1.1", | ||
| nodeOptions: { agent }, | ||
| }); | ||
| } | ||
| // No proxy: connect directly over HTTP/2. | ||
| return createHttp2Transport(baseUrl); | ||
| } | ||
| /** | ||
| * Build a direct HTTP/2 transport with an optimistically pre-connecting session | ||
| * manager. | ||
| * | ||
| * When `createConnection` is supplied the session is tunneled through it (used | ||
| * to route HTTP/2 through a proxy via `CONNECT`); otherwise it connects directly | ||
| * to `baseUrl`. Either way pings and the idle timeout behave identically — only | ||
| * the underlying connection differs. | ||
| * | ||
| * @param baseUrl | ||
| * Base URI for all HTTP requests. | ||
| * @param createConnection | ||
| * Optional connection factory passed through to `http2.connect` (optional). | ||
| * @returns | ||
| * Connect transport that talks HTTP/2 to `baseUrl`. | ||
| */ | ||
| function createHttp2Transport(baseUrl, createConnection) { | ||
| const sessionManager = new Http2SessionManager(baseUrl, | ||
| // AWS Global Accelerator doesn't support PING so we use a very high idle | ||
| // timeout. Ref: | ||
| // https://docs.aws.amazon.com/global-accelerator/latest/dg/introduction-how-it-works.html#about-idle-timeout | ||
| { idleConnectionTimeoutMs: 340 * 1000 }, createConnection ? { createConnection } : undefined); | ||
| // This is an optimistic pre-connect. In Deno, the Node HTTP/2 compatibility | ||
| // layer can surface background session failures as uncaught test errors, so | ||
| // we only warm the connection in Node. | ||
| if (!("Deno" in globalThis)) { | ||
| sessionManager.connect(); | ||
| } | ||
| return createConnectTransport({ baseUrl, httpVersion: "2", sessionManager }); | ||
| } | ||
| export { createTransport }; |
+20
-9
| { | ||
| "name": "@arcjet/transport", | ||
| "version": "1.5.0", | ||
| "version": "1.6.0", | ||
| "description": "Transport mechanisms for the Arcjet protocol", | ||
@@ -28,3 +28,3 @@ "keywords": [ | ||
| "engines": { | ||
| "node": ">=20" | ||
| "node": ">=22.21.0 <23 || >=24.5.0" | ||
| }, | ||
@@ -36,2 +36,3 @@ "type": "module", | ||
| "bun": "./bun.js", | ||
| "deno": "./deno.js", | ||
| "edge-light": "./edge-light.js", | ||
@@ -44,2 +45,6 @@ "workerd": "./workerd.js", | ||
| "bun.js", | ||
| "deno.d.ts", | ||
| "deno.js", | ||
| "detect-proxy.d.ts", | ||
| "detect-proxy.js", | ||
| "edge-light.d.ts", | ||
@@ -49,2 +54,4 @@ "edge-light.js", | ||
| "index.js", | ||
| "proxy-tunnel.d.ts", | ||
| "proxy-tunnel.js", | ||
| "workerd.d.ts", | ||
@@ -58,15 +65,19 @@ "workerd.js" | ||
| "test-coverage": "node --experimental-test-coverage --test -- test/*.test.js", | ||
| "test-runtime-bun": "npm run build && HTTPS_PROXY=http://127.0.0.1:49219 bun test test/runtime/proxy.bun.test.ts", | ||
| "test-runtime-deno": "npm run build && HTTPS_PROXY=http://127.0.0.1:49219 deno test --allow-all --no-check test/runtime/proxy.deno.test.ts", | ||
| "test": "npm run build && npm run lint && npm run test-coverage" | ||
| }, | ||
| "dependencies": { | ||
| "@arcjet/env": "1.6.0", | ||
| "@arcjet/logger": "1.6.0", | ||
| "@bufbuild/protobuf": "2.12.0", | ||
| "@connectrpc/connect": "2.1.1", | ||
| "@connectrpc/connect-node": "2.1.1", | ||
| "@connectrpc/connect-web": "2.1.1" | ||
| "@connectrpc/connect": "2.1.2", | ||
| "@connectrpc/connect-node": "2.1.2", | ||
| "@connectrpc/connect-web": "2.1.2" | ||
| }, | ||
| "devDependencies": { | ||
| "@arcjet/eslint-config": "1.5.0", | ||
| "@arcjet/rollup-config": "1.5.0", | ||
| "@rollup/wasm-node": "4.61.0", | ||
| "@types/node": "24.12.4", | ||
| "@arcjet/eslint-config": "1.6.0", | ||
| "@arcjet/rollup-config": "1.6.0", | ||
| "@rollup/wasm-node": "4.62.2", | ||
| "@types/node": "22.19.21", | ||
| "eslint": "9.39.4", | ||
@@ -73,0 +84,0 @@ "typescript": "5.9.3" |
+118
-8
@@ -45,2 +45,13 @@ <!-- trunk-ignore-all(markdownlint/MD001) --> | ||
| ## Node.js version support | ||
| This package requires `>=22.21.0 <23 || >=24.5.0`. Proxy support relies on the | ||
| built-in proxy support of the Node.js HTTP agent, which is only available on | ||
| Node.js `>=22.21.0` and, on the 24 line, `>=24.5.0`. Node.js 20 is end-of-life | ||
| and Node.js 23 is not supported. Anyone tracking an active LTS release is | ||
| unaffected. | ||
| Because every Arcjet SDK depends on this package, the same requirement applies | ||
| across the Arcjet SDKs. | ||
| ## Use | ||
@@ -61,12 +72,73 @@ | ||
| This package exports no [TypeScript][] types. | ||
| This package exports the [TypeScript][] types | ||
| [`ProxyEnvironment`][api-proxy-environment], | ||
| [`TransportLogger`][api-transport-logger], and | ||
| [`TransportOptions`][api-transport-options]. | ||
| ### `createTransport(baseUrl)` | ||
| ### `createTransport(baseUrl[, options])` | ||
| Creates a transport that talks over HTTP/2 using | ||
| `@connectrpc/connect-node`. This is a thin wrapper around | ||
| [`createConnectTransport`][connect-create-transport]. | ||
| Alternative entry points exist for Bun, Edge Light, and `workerd` that use | ||
| `@connectrpc/connect-web` instead. | ||
| Creates a transport that talks to the Arcjet API. On Node.js it uses | ||
| `@connectrpc/connect-node` over HTTP/2; separate entry points for Bun, Deno, | ||
| Edge Light, and `workerd` use `@connectrpc/connect-web` instead. This is a thin | ||
| wrapper around [`createConnectTransport`][connect-create-transport]. | ||
| ### Proxy support | ||
| The standard proxy environment variables (`HTTP_PROXY` and `HTTPS_PROXY`, while | ||
| respecting `NO_PROXY`) are auto-detected, making it possible to connect to the | ||
| Arcjet API through a proxy such as [Squid][squid]. When a proxy is in use, a | ||
| line is logged at startup at `info` level (so set `ARCJET_LOG_LEVEL=info` to see | ||
| it). The proxy URL itself is not logged, since it can contain credentials. How | ||
| the request is actually proxied depends on the runtime, using each runtime's | ||
| built-in proxy support: | ||
| - **Node.js** — requests are routed through the proxy over HTTP/1.1 using the | ||
| built-in proxy support of the Node.js HTTP agent; otherwise they are made | ||
| directly over HTTP/2. Set `proxyHttpVersion: "2"` to instead keep HTTP/2 while | ||
| proxying (see [HTTP/2 through a proxy](#http2-through-a-proxy) below). | ||
| - **Bun** and **Deno** — the runtime's `fetch` performs the proxying natively. | ||
| - **Edge Light** and **`workerd`** — these edge runtimes don't support outbound | ||
| proxy environment variables, so no proxy is used. | ||
| `NO_PROXY` accepts a comma- or space-separated list of host suffixes, each with | ||
| an optional leading `.` or `*.` and an optional `:port`, plus `*` to bypass the | ||
| proxy for every host. Entries are matched as host names; IP/CIDR ranges (such as | ||
| `10.0.0.0/8`) are not supported, the same as [curl][curl-noproxy]. On Bun and | ||
| Deno the runtime's `fetch` applies `NO_PROXY` itself, so its exact semantics are | ||
| the runtime's. | ||
| #### HTTP/2 through a proxy | ||
| By default, proxying on Node.js downgrades the connection from HTTP/2 to | ||
| HTTP/1.1, because Node's built-in agent proxy support only works over HTTP/1.1. | ||
| For a latency-sensitive API this is unfortunate: it gives up HTTP/2's | ||
| multiplexing, so a burst of concurrent requests opens a new proxy connection | ||
| each instead of sharing one. | ||
| Setting `proxyHttpVersion: "2"` keeps HTTP/2 end-to-end. The transport opens an | ||
| HTTP `CONNECT` tunnel to the proxy and then performs the TLS handshake — and the | ||
| ALPN negotiation that selects `h2` — directly with the origin. The proxy only | ||
| blindly forwards the tunnel, so it never sees, and cannot downgrade, the | ||
| negotiated protocol. | ||
| This comes with caveats: | ||
| - **Node.js only.** Bun and Deno don't implement the agent option this builds | ||
| on; they proxy through their `fetch` (over HTTP/1.1) regardless of this | ||
| setting, and the edge runtimes don't proxy at all. | ||
| - **Requires a tunneling (`CONNECT`) proxy** — the common kind for HTTPS egress, | ||
| including [Squid][squid]. A proxy that terminates TLS and re-originates an | ||
| HTTP/1.1 connection to the origin (a TLS-intercepting / "MITM" proxy) cannot | ||
| preserve HTTP/2 no matter what this option is set to. | ||
| - **The proxy must not buffer the tunnel.** HTTP/2 sends many small, dependent | ||
| frames. The transport disables [Nagle's algorithm][nagle] (`TCP_NODELAY`) on | ||
| its side of the tunnel, but if the proxy buffers tunneled bytes (or leaves | ||
| Nagle enabled on its upstream socket) the interaction with delayed ACKs can | ||
| add roughly 40 ms of latency per round trip, erasing the benefit. | ||
| Tunneling proxies such as Squid set `TCP_NODELAY` on `CONNECT` tunnels by | ||
| default; verify this if you use a different proxy. | ||
| When no proxy applies, this option has no effect — direct connections always use | ||
| HTTP/2. | ||
| ###### Parameters | ||
@@ -76,2 +148,4 @@ | ||
| — the base URL for all HTTP requests | ||
| - `options` ([`TransportOptions`][api-transport-options], optional) | ||
| — configuration | ||
@@ -83,2 +157,32 @@ ###### Returns | ||
| ### `ProxyEnvironment` | ||
| Map of environment variables used to detect an outbound proxy (TypeScript | ||
| type). This is the same shape as `process.env`. | ||
| ### `TransportLogger` | ||
| Logger used to print a line at startup when a proxy is detected (TypeScript | ||
| type). It must provide an `info` method. | ||
| ### `TransportOptions` | ||
| Configuration for `createTransport` (TypeScript type). | ||
| ###### Fields | ||
| - `log` ([`TransportLogger`][api-transport-logger], optional) | ||
| — logger used to print a line at startup when a proxy is detected; defaults | ||
| to a logger configured from the `ARCJET_LOG_LEVEL` environment variable | ||
| - `proxyEnv` ([`ProxyEnvironment`][api-proxy-environment] or `false`, optional) | ||
| — environment variables used to detect an outbound proxy; defaults to | ||
| `process.env` so standard proxy environment variables are auto-detected; pass | ||
| `false` to ignore proxy environment variables | ||
| - `proxyHttpVersion` (`"1.1"` or `"2"`, optional, default `"1.1"`) | ||
| — HTTP version to use when a proxy is in use on Node.js; `"1.1"` routes | ||
| through the proxy using the Node.js HTTP agent, while `"2"` keeps HTTP/2 by | ||
| tunneling through the proxy with `CONNECT`; has no effect without a proxy, or | ||
| on Bun, Deno, and the edge runtimes (see | ||
| [HTTP/2 through a proxy](#http2-through-a-proxy)) | ||
| ## License | ||
@@ -89,6 +193,12 @@ | ||
| [apache-license]: http://www.apache.org/licenses/LICENSE-2.0 | ||
| [api-create-transport]: #createtransportbaseurl | ||
| [api-create-transport]: #createtransportbaseurl-options | ||
| [api-proxy-environment]: #proxyenvironment | ||
| [api-transport-logger]: #transportlogger | ||
| [api-transport-options]: #transportoptions | ||
| [arcjet]: https://arcjet.com | ||
| [arcjet-get-started]: https://docs.arcjet.com/get-started | ||
| [connect-create-transport]: https://connectrpc.com/docs/web/choosing-a-protocol/ | ||
| [curl-noproxy]: https://curl.se/docs/manpage.html#--noproxy | ||
| [nagle]: https://en.wikipedia.org/wiki/Nagle%27s_algorithm | ||
| [squid]: https://www.squid-cache.org/ | ||
| [typescript]: https://www.typescriptlang.org/ |
+4
-1
@@ -1,1 +0,4 @@ | ||
| export declare function createTransport(baseUrl: string): import("@connectrpc/connect").Transport; | ||
| import type { Transport } from "@connectrpc/connect"; | ||
| export type { ProxyEnvironment, TransportLogger, TransportOptions, } from "./detect-proxy.js"; | ||
| import type { TransportOptions } from "./detect-proxy.js"; | ||
| export declare function createTransport(baseUrl: string, _options?: TransportOptions): Transport; |
+5
-15
| import { createConnectTransport } from '@connectrpc/connect-web'; | ||
| // This file is used when running on the `workerd`. | ||
| // Specifically workers on Cloudflare. | ||
| // It is the same as `edge-light.ts`, which runs on Vercel. | ||
| // It uses DOM based APIs (`@connectrpc/connect-web`) to connect to the API. | ||
| // Differing from `bun.ts` this solves the `redirect` option set to `error` | ||
| // inside `connect` as that does not work on the edge. | ||
| // | ||
| // For more information, see: | ||
| // | ||
| // * <https://github.com/connectrpc/connect-es/pull/589> | ||
| // * <https://github.com/connectrpc/connect-es/issues/749#issuecomment-1693507516> | ||
| // * <https://github.com/connectrpc/connect-es/pull/1082> | ||
| // * <https://github.com/e2b-dev/E2B/pull/669/files> | ||
| // * <https://github.com/connectrpc/connect-es/issues/577#issuecomment-2210103503> | ||
| function createTransport(baseUrl) { | ||
| function createTransport(baseUrl, | ||
| // These edge runtimes don't support outbound proxy environment variables, so | ||
| // the options are accepted for API parity with the other entry points but no | ||
| // proxy is detected or used. | ||
| _options) { | ||
| return createConnectTransport({ | ||
@@ -19,0 +9,0 @@ baseUrl, |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 2 instances
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
57269
183.82%17
54.55%756
641.18%200
122.22%6
50%4
Infinity%7
75%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
- Removed
- Removed
- Removed
Updated