@dudousxd/nestjs-telescope
Version:
Laravel Telescope-style observability console for NestJS — core: watchers, recorder, correlation, SQLite store, headless API.
94 lines • 3.96 kB
JavaScript
const REPLAY_TIMEOUT_MS = 30_000;
/** Max bytes of the replayed response body returned (4 KB). */
const REPLAY_BODY_CAP = 4096;
/** Headers stripped from the replayed request (auth/session/routing leakage). */
const REPLAY_STRIPPED_HEADERS = new Set(['cookie', 'authorization', 'host', 'content-length']);
/**
* Re-issue a captured request against the LOCAL server (127.0.0.1:<port>) so a
* developer can reproduce it from the dashboard. The replay carries a
* `x-telescope-replay: 1` header (so the host can recognize/skip it) and strips
* cookie/authorization/host headers — a replay must not silently reuse the
* original caller's credentials. Bounded by a 30s timeout; the body is capped at
* 4 KB. Never throws — a failed call returns `status: 0` with an `error`.
*/
export async function replayRequest(content, request) {
const port = resolveLocalPort(request);
const path = content.uri.startsWith('/') ? content.uri : `/${content.uri}`;
const url = `http://127.0.0.1:${port}${path}`;
const headers = buildReplayHeaders(content.headers);
const method = (content.method || 'GET').toUpperCase();
const hasBody = method !== 'GET' && method !== 'HEAD' && content.payload != null;
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), REPLAY_TIMEOUT_MS);
const startedAt = Date.now();
try {
const response = await fetch(url, {
method,
headers,
...(hasBody ? { body: serializePayload(content.payload, headers) } : {}),
signal: controller.signal,
redirect: 'manual',
});
const text = await response.text();
return {
status: response.status,
durationMs: Date.now() - startedAt,
body: text.slice(0, REPLAY_BODY_CAP),
};
}
catch (error) {
return {
status: 0,
durationMs: Date.now() - startedAt,
body: '',
error: error instanceof Error ? error.message : String(error),
};
}
finally {
clearTimeout(timer);
}
}
/** Copy headers minus the stripped set, and force `x-telescope-replay: 1`. */
function buildReplayHeaders(source) {
const headers = {};
for (const [key, value] of Object.entries(source)) {
if (REPLAY_STRIPPED_HEADERS.has(key.toLowerCase()))
continue;
if (typeof value === 'string')
headers[key] = value;
else if (Array.isArray(value) && typeof value[0] === 'string')
headers[key] = value[0];
}
headers['x-telescope-replay'] = '1';
return headers;
}
/** Serialize a captured payload to a fetch body, defaulting to JSON. */
function serializePayload(payload, headers) {
if (typeof payload === 'string')
return payload;
const hasContentType = Object.keys(headers).some((k) => k.toLowerCase() === 'content-type');
if (!hasContentType)
headers['content-type'] = 'application/json';
return JSON.stringify(payload);
}
/** Resolve the local server port from the incoming request's socket/host header. */
function resolveLocalPort(request) {
if (typeof request === 'object' && request !== null) {
const socket = request.socket;
if (socket && typeof socket.localPort === 'number' && socket.localPort > 0) {
return socket.localPort;
}
const headers = request.headers;
const host = headers?.host;
if (typeof host === 'string') {
const portPart = host.split(':')[1];
const parsed = portPart !== undefined ? Number(portPart) : Number.NaN;
if (Number.isFinite(parsed) && parsed > 0)
return parsed;
}
}
// Fall back to PORT env or the conventional 3000.
const envPort = Number(process.env.PORT);
return Number.isFinite(envPort) && envPort > 0 ? envPort : 3000;
}
//# sourceMappingURL=request-replay.js.map