🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

otterkit

Package Overview
Dependencies
Maintainers
1
Versions
41
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

otterkit - npm Package Compare versions

Comparing version
0.31.0
to
0.32.0
+49
-35
dist/replay.js

@@ -16,3 +16,3 @@ /**

import { cleanupDaemons } from './daemon.js';
import { readLog, logRequest, getLogPath } from './request-log.js';
import { readLog, getLogPath } from './request-log.js';
import { getProvider } from './providers.js';

@@ -55,3 +55,8 @@ /* ── ANSI helpers (mirrors index.ts) ── */

'proxy-authenticate',
// Edge-injected metadata: replaying these back at Cloudflare trips its
// WAF (a 403 before the request ever reaches the endpoint).
'x-real-ip',
'cdn-loop',
]);
const SKIP_HEADER_PREFIXES = ['cf-', 'x-forwarded-'];
const REPLAY_TIMEOUT_MS = 30_000;

@@ -110,9 +115,18 @@ /** Thrown by replayCore for expected failures; carries a user-facing hint. */

}
/**
* Accepts `host:port`, `http://host:port`, and a full `https://host` URL -
* the last one matters for replaying back through a public endpoint, where
* the scheme (and its default port) must survive.
*/
function parseTarget(target) {
const match = target.match(/^(?:https?:\/\/)?([^:/]+)(?::(\d+))?\/?$/);
const port = match?.[2] ? parseInt(match[2], 10) : NaN;
if (!match || isNaN(port) || port < 1 || port > 65535) {
const match = target.match(/^(?:(https?):\/\/)?([^:/]+)(?::(\d+))?\/?$/);
if (!match) {
throw new ReplayError(`Invalid target "${target}"`, 'Expected host:port or a URL, e.g. --target 127.0.0.1:3000');
}
const scheme = match[1] ?? 'http';
const port = match[3] ? parseInt(match[3], 10) : scheme === 'https' ? 443 : NaN;
if (isNaN(port) || port < 1 || port > 65535) {
throw new ReplayError(`Invalid target "${target}"`, 'Expected host:port, e.g. --target 127.0.0.1:3000');
}
return { host: match[1], port };
return { scheme, host: match[2], port };
}

@@ -124,3 +138,3 @@ function resolveTarget(subdomain, targetOpt) {

if (daemon)
return { host: daemon.host, port: daemon.port };
return { scheme: 'http', host: daemon.host, port: daemon.port };
throw new ReplayError('No target to replay to', `No running tunnel daemon for "${subdomain}". Pass --target <host:port>.`);

@@ -146,3 +160,3 @@ }

const { entry, n } = pickEntry(entries, options.index ?? '-1');
const { host, port } = resolveTarget(subdomain, options.target);
const { scheme, host, port } = resolveTarget(subdomain, options.target);
// Rebuild the request, applying any overrides.

@@ -153,4 +167,6 @@ const method = (options.method || entry.method).toUpperCase();

for (const [key, value] of Object.entries(entry.headers)) {
if (!SKIP_HEADERS.has(key.toLowerCase()))
headers[key.toLowerCase()] = value;
const k = key.toLowerCase();
if (SKIP_HEADERS.has(k) || SKIP_HEADER_PREFIXES.some(prefix => k.startsWith(prefix)))
continue;
headers[k] = value;
}

@@ -213,15 +229,24 @@ for (const override of options.header ?? []) {

}
const targetUrl = `http://${host}:${port}${path}`;
// Dev servers often bind IPv6 localhost only - try both loopbacks.
const hosts = host === '127.0.0.1' || host === 'localhost' ? [host, '[::1]'] : [host];
const started = Date.now();
let resp;
try {
resp = await fetch(targetUrl, {
method,
headers,
body,
redirect: 'manual',
signal: AbortSignal.timeout(REPLAY_TIMEOUT_MS),
});
let lastErr;
for (const candidate of hosts) {
try {
resp = await fetch(`${scheme}://${candidate}:${port}${path}`, {
method,
headers,
body,
redirect: 'manual',
signal: AbortSignal.timeout(REPLAY_TIMEOUT_MS),
});
break;
}
catch (err) {
lastErr = err;
}
}
catch (err) {
if (!resp) {
const err = lastErr;
const reason = err instanceof Error && err.name === 'TimeoutError' ? 'Timed out' : 'Failed';

@@ -236,18 +261,7 @@ throw new ReplayError(`${reason} replaying to ${host}:${port}`, `Is the target running? ${err instanceof Error ? (err.cause instanceof Error ? err.cause.message : err.message) : ''}`);

});
// Append the replayed exchange to the log so `otterkit inspect` shows it,
// matching how desktop replay emits a fresh traffic entry.
await logRequest(subdomain, {
ts: new Date().toISOString(),
method,
path,
headers,
body: body ? body.toString('base64') : null,
status: resp.status,
durationMs,
bodySize: body?.length ?? 0,
response: {
headers: respHeaders,
body: respBody.length > 0 ? respBody.toString('base64') : null,
},
}).catch(() => { });
// The replay is NOT written back to the capture log. The log records what
// arrived at the endpoint: a replay to the public URL is captured there
// for real (writing it too would double it), and a replay straight to
// localhost never reached the endpoint at all - logging it would invent
// traffic. Callers get the outcome from the return value below.
return {

@@ -254,0 +268,0 @@ index: n,

{
"name": "otterkit",
"version": "0.31.0",
"version": "0.32.0",
"description": "OtterKit CLI - provision and connect tunnels for AI agents",

@@ -5,0 +5,0 @@ "mcpName": "io.github.useotterkit/otterkit",