Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

@useswarm/cli

Package Overview
Dependencies
Maintainers
1
Versions
12
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@useswarm/cli - npm Package Compare versions

Comparing version
0.3.5
to
0.3.7
+26
-11
dist/commands/test.js

@@ -418,3 +418,7 @@ import { Command } from "commander";

const frontendPort = parsedUrl?.port ? parseInt(parsedUrl.port, 10) : 3000;
let tunnelPort = frontendPort;
// The local origin we'll spoof on outbound Origin/Referer headers so the
// user's CSRF middleware sees a same-origin request. Falls back to the
// frontend port if --url somehow lacked one.
const localOrigin = parsedUrl?.origin ?? `http://localhost:${frontendPort}`;
let backendPort;
// Pre-flight connectivity check

@@ -432,5 +436,4 @@ const frontendReachable = await checkPort(frontendPort);

}
// Start reverse proxy if backend provided
if (backendUrl) {
let backendPort = 8080;
backendPort = 8080;
if (/^\d+$/.test(backendUrl)) {

@@ -457,6 +460,14 @@ backendPort = parseInt(backendUrl, 10);

}
const proxySpinner = ora("Starting reverse proxy...").start();
try {
proxy = await startProxy(frontendPort, backendPort, backendPaths);
tunnelPort = proxy.port;
}
// We now ALWAYS run the local proxy in front of the user's app, even
// when --backend isn't set. The proxy's only job in the no-backend case
// is to spoof the Origin/Referer headers from the tunnel host back to
// the user's local origin, so their CSRF middleware (Better Auth /
// NextAuth / Django / etc.) accepts the agent's POSTs without the user
// having to add "*.trycloudflare.com" / "*.ngrok-free.app" to their
// trustedOrigins. Routing-wise it's a transparent passthrough.
const proxySpinner = ora("Starting local proxy...").start();
try {
proxy = await startProxy(frontendPort, backendPort ?? frontendPort, backendPaths, localOrigin);
if (backendPort != null) {
proxySpinner.succeed(`Reverse proxy started\n` +

@@ -466,8 +477,12 @@ ` Frontend: ${chalk.cyan(`localhost:${frontendPort}`)}\n` +

}
catch (err) {
proxySpinner.fail("Failed to start reverse proxy");
console.error(chalk.red(err.message));
process.exit(1);
else {
proxySpinner.succeed(`Local proxy ready ${chalk.dim(`(spoofing Origin → ${localOrigin})`)}`);
}
}
catch (err) {
proxySpinner.fail("Failed to start local proxy");
console.error(chalk.red(err.message));
process.exit(1);
}
const tunnelPort = proxy.port;
const tunnelProvider = opts.tunnelProvider ?? "auto";

@@ -474,0 +489,0 @@ const providerLabel = tunnelProvider === "auto" ? "" : ` (${tunnelProvider})`;

@@ -6,2 +6,2 @@ export interface ProxyInfo {

}
export declare function startProxy(frontendPort: number, backendPort: number, backendPaths?: string[]): Promise<ProxyInfo>;
export declare function startProxy(frontendPort: number, backendPort: number, backendPaths?: string[], localOrigin?: string): Promise<ProxyInfo>;
import http from "node:http";
import zlib from "node:zlib";
import { Readable } from "node:stream";
import httpProxy from "http-proxy";
// Cap on how much of an inbound request body we'll buffer for tunnel-URL
// rewriting. 5 MB covers every realistic auth/JSON payload while keeping us
// far away from any "buffer the whole upload" footgun. Anything larger is
// streamed straight through unmodified.
const MAX_REQUEST_REWRITE_BYTES = 5 * 1024 * 1024;
/** True if a content-type is text-shaped enough to safely substring-replace. */
function isRewritableRequestBody(contentType) {
if (!contentType)
return false;
const ct = contentType.toLowerCase();
return (ct.startsWith("application/json") ||
ct.startsWith("application/x-www-form-urlencoded") ||
ct.startsWith("text/"));
}
const DEFAULT_BACKEND_PATH_PREFIXES = ["/api", "/auth", "/graphql", "/trpc"];

@@ -203,4 +218,45 @@ // ─── HMR / dev-only path prefixes ────────────────────────────────────────────

}
// Rewrite the incoming Origin/Referer headers to look like they came from the
// user's own local origin. The browser's request URL (and the upstream Host)
// are not touched — only the headers that CSRF middleware inspects. This makes
// the agent's tunneled-test traffic indistinguishable from a same-origin
// request to the user's app, so frameworks like Better Auth / NextAuth / Django
// accept it without the user needing to add any tunnel-specific entry to their
// `trustedOrigins` / `CSRF_TRUSTED_ORIGINS` / equivalent.
function spoofOriginHeaders(headers, localOrigin) {
if (!localOrigin)
return;
let parsedLocal;
try {
parsedLocal = new URL(localOrigin);
}
catch {
return;
}
if (typeof headers.origin === "string" && headers.origin.length > 0 && headers.origin !== "null") {
headers.origin = parsedLocal.origin;
}
const referer = headers.referer ?? headers.referrer;
if (typeof referer === "string" && referer.length > 0) {
try {
const refUrl = new URL(referer);
refUrl.protocol = parsedLocal.protocol;
refUrl.host = parsedLocal.host;
headers.referer = refUrl.toString();
delete headers.referrer;
}
catch {
// Malformed referer — leave it alone.
}
}
}
// ─── Public API ──────────────────────────────────────────────────────────────
export async function startProxy(frontendPort, backendPort, backendPaths) {
export async function startProxy(frontendPort, backendPort, backendPaths,
// Origin to spoof on incoming requests. Set this to the user's original local
// URL (e.g., "http://localhost:3000") so Better Auth / NextAuth / Django CSRF
// / any same-origin-style check on the user's app sees its own configured
// origin and accepts the agent's POSTs — instead of seeing the tunnel host
// (`xyz.trycloudflare.com`) and rejecting with "Invalid origin". The end user
// doesn't need to know about tunnel hostnames or touch their auth config.
localOrigin) {
let tunnelUrl;

@@ -410,3 +466,69 @@ const BACKEND_PATH_PREFIXES = backendPaths

req.headers["x-forwarded-host"] = req.headers["host"] || "";
proxy.web(req, res, { target });
spoofOriginHeaders(req.headers, localOrigin);
// Rewrite tunnel-URL occurrences inside the request body back to the user's
// local origin. Frontends frequently embed `window.location.origin` into
// POST payloads (Better Auth / NextAuth `callbackURL`, redirect URLs, OAuth
// state, etc.) and any framework-side trustedOrigins check on those values
// would reject the tunnel host the same way it would reject a tunnel-host
// Origin header. The proxy already does the inverse rewrite on responses
// (localhost → tunnel) so the round-trip stays consistent.
const method = (req.method ?? "GET").toUpperCase();
const hasBody = method !== "GET" && method !== "HEAD" && method !== "OPTIONS";
const shouldRewriteBody = hasBody && tunnelUrl && localOrigin && isRewritableRequestBody(Array.isArray(req.headers["content-type"]) ? req.headers["content-type"][0] : req.headers["content-type"]);
if (!shouldRewriteBody) {
proxy.web(req, res, { target });
return;
}
const chunks = [];
let total = 0;
let exceeded = false;
req.on("data", (chunk) => {
if (exceeded)
return;
total += chunk.length;
if (total > MAX_REQUEST_REWRITE_BYTES) {
// Body too big to rewrite safely. Replay what we buffered as-is and
// stream the rest straight through — large uploads aren't auth payloads.
exceeded = true;
const replay = new Readable({ read() { } });
for (const c of chunks)
replay.push(c);
replay.push(chunk);
req.on("data", (rest) => replay.push(rest));
req.on("end", () => replay.push(null));
req.on("error", (e) => replay.destroy(e));
proxy.web(req, res, { target, buffer: replay });
chunks.length = 0;
return;
}
chunks.push(chunk);
});
req.on("error", (err) => {
if (!res.headersSent)
res.writeHead(400);
res.end(`Bad request body: ${err.message}`);
});
req.on("end", () => {
if (exceeded)
return;
const original = Buffer.concat(chunks);
let modified = original;
try {
const text = original.toString("utf8");
// Replace both the exact tunnel URL and any subdomain/host reference
// a frontend might have grabbed from window.location.origin.
const rewritten = text.split(tunnelUrl).join(localOrigin);
if (rewritten !== text) {
modified = Buffer.from(rewritten, "utf8");
}
}
catch {
// Non-UTF8 body slipped past content-type — forward unchanged.
}
// Content-Length must reflect the modified payload size or upstream
// frameworks will hang waiting for bytes that never arrive.
req.headers["content-length"] = String(modified.length);
const buffered = Readable.from(modified);
proxy.web(req, res, { target, buffer: buffered });
});
});

@@ -425,2 +547,3 @@ // ── WebSocket upgrade ─────────────────────────────────────────────────────

: `http://127.0.0.1:${frontendPort}`;
spoofOriginHeaders(req.headers, localOrigin);
proxy.ws(req, socket, head, { target }, (err) => {

@@ -427,0 +550,0 @@ if (err)

{
"name": "@useswarm/cli",
"version": "0.3.5",
"version": "0.3.7",
"description": "Swarm CLI — AI-powered UX testing from your terminal",

@@ -5,0 +5,0 @@ "type": "module",