417 lines
16 KiB
JavaScript
417 lines
16 KiB
JavaScript
|
|
"use strict";
|
|||
|
|
|
|||
|
|
const { TextDecoder, TextEncoder } = require("node:util");
|
|||
|
|
const { performance } = require("node:perf_hooks");
|
|||
|
|
const {
|
|||
|
|
TERMINAL_INTERCEPTOR_MAX_CHUNK_BYTES,
|
|||
|
|
TERMINAL_INTERCEPTOR_MAX_WINDOW_BYTES,
|
|||
|
|
createTerminalInterceptorEnvelope,
|
|||
|
|
} = require("../plugins/terminalInterceptorTransport.cjs");
|
|||
|
|
|
|||
|
|
const INPUT_DEADLINE_MS = 4;
|
|||
|
|
const OUTPUT_DEADLINE_MS = 50;
|
|||
|
|
const MAX_CHUNK_BYTES = TERMINAL_INTERCEPTOR_MAX_CHUNK_BYTES;
|
|||
|
|
const OUTPUT_WINDOW_BYTES = TERMINAL_INTERCEPTOR_MAX_WINDOW_BYTES;
|
|||
|
|
const PROMPT_TAIL_CHARS = 2_048;
|
|||
|
|
const SENSITIVE_LABELS = [
|
|||
|
|
"pass(?:word|phrase|code)", "passwd", "one[\\s-]?time", "otp", "verification",
|
|||
|
|
"authentication", "security[\\s-]+(?:code|token|passcode|pin)", "\\bpin\\b",
|
|||
|
|
"\\btoken\\b", "2fa", "two[\\s-]?factor", "multi[\\s-]?factor", "\\bmfa\\b",
|
|||
|
|
"second[\\s-]+factor", "secondary", "re[\\s-]?enter", "confirm", "\\bedr\\b",
|
|||
|
|
"\\bduo\\b", "密码", "密碼", "口令", "动态", "動態", "一次性", "验证码",
|
|||
|
|
"驗證碼", "验证信息", "驗證資訊", "令牌", "双因素", "雙因素", "多因素",
|
|||
|
|
"短信验证", "簡訊驗證", "手机验证", "手機驗證", "二次", "安全密码", "安全密碼",
|
|||
|
|
"挑战码", "挑戰碼", "парол",
|
|||
|
|
].join("|");
|
|||
|
|
const SENSITIVE_PROMPT = new RegExp(
|
|||
|
|
`(?:${SENSITIVE_LABELS})[^\\r\\n]{0,160}[::?>›»]?\\s*$`,
|
|||
|
|
"iu",
|
|||
|
|
);
|
|||
|
|
const CONFIRMED_SHELL_PROMPT = /^(?:(?:~)?[#$%]|(?:[^\r\n]{1,120}@[^\r\n]{1,120}:[^\r\n]{0,120}[#$%])|(?:PS(?:\s+\S.*)?>))\s*$/u;
|
|||
|
|
|
|||
|
|
function messageData(value) {
|
|||
|
|
return value && typeof value === "object" && "data" in value ? value.data : value;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function addPortListener(port, listener) {
|
|||
|
|
if (typeof port.addEventListener === "function") {
|
|||
|
|
port.addEventListener("message", listener);
|
|||
|
|
port.start?.();
|
|||
|
|
return () => port.removeEventListener?.("message", listener);
|
|||
|
|
}
|
|||
|
|
port.on?.("message", listener);
|
|||
|
|
port.start?.();
|
|||
|
|
return () => port.off?.("message", listener) ?? port.removeListener?.("message", listener);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function toTransferBuffer(bytes) {
|
|||
|
|
const copy = new Uint8Array(bytes.byteLength);
|
|||
|
|
copy.set(bytes);
|
|||
|
|
return copy.buffer;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function nextUtf8ChunkEnd(bytes, offset, maxChunkBytes = MAX_CHUNK_BYTES) {
|
|||
|
|
let end = Math.min(bytes.byteLength, offset + maxChunkBytes);
|
|||
|
|
if (end >= bytes.byteLength) return end;
|
|||
|
|
while (end > offset && (bytes[end] & 0xc0) === 0x80) end -= 1;
|
|||
|
|
if (end > offset) return end;
|
|||
|
|
// TextEncoder output is valid UTF-8 and a code point is at most four bytes,
|
|||
|
|
// so this branch is only reachable with an artificially tiny test limit.
|
|||
|
|
end = Math.min(bytes.byteLength, offset + maxChunkBytes);
|
|||
|
|
while (end < bytes.byteLength && (bytes[end] & 0xc0) === 0x80) end += 1;
|
|||
|
|
return end;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function visibleTerminalTail(value) {
|
|||
|
|
return String(value ?? "")
|
|||
|
|
.replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/gu, "")
|
|||
|
|
.replace(/\x1b\[[0-?]*[ -/]*[@-~]/gu, "")
|
|||
|
|
.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/gu, "")
|
|||
|
|
.slice(-PROMPT_TAIL_CHARS);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function createTerminalDataPipeline(options = {}) {
|
|||
|
|
const encoder = options.encoder ?? new TextEncoder();
|
|||
|
|
const decoder = options.decoder ?? new TextDecoder("utf-8", { fatal: true });
|
|||
|
|
const now = options.now ?? (() => performance.now());
|
|||
|
|
const onWarning = options.onWarning ?? (() => {});
|
|||
|
|
const inputDeadlineMs = options.inputDeadlineMs ?? INPUT_DEADLINE_MS;
|
|||
|
|
const outputDeadlineMs = options.outputDeadlineMs ?? OUTPUT_DEADLINE_MS;
|
|||
|
|
const outputWindowBytes = options.outputWindowBytes ?? OUTPUT_WINDOW_BYTES;
|
|||
|
|
const bindings = new Map();
|
|||
|
|
const outputModes = new Map();
|
|||
|
|
const outputRawTails = new Map();
|
|||
|
|
const sensitiveInputSessions = new Set();
|
|||
|
|
|
|||
|
|
const keyOf = (sessionId, direction) => `${sessionId}\0${direction}`;
|
|||
|
|
|
|||
|
|
function refreshOutputMode(sessionId) {
|
|||
|
|
const input = bindings.has(keyOf(sessionId, "input"));
|
|||
|
|
const output = bindings.has(keyOf(sessionId, "output"));
|
|||
|
|
const mode = (input ? 1 : 0) | (output ? 2 : 0);
|
|||
|
|
if (mode) outputModes.set(sessionId, mode);
|
|||
|
|
else outputModes.delete(sessionId);
|
|||
|
|
if (!mode) {
|
|||
|
|
outputRawTails.delete(sessionId);
|
|||
|
|
sensitiveInputSessions.delete(sessionId);
|
|||
|
|
} else if (!input) {
|
|||
|
|
sensitiveInputSessions.delete(sessionId);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function warn(binding, code, message) {
|
|||
|
|
try {
|
|||
|
|
onWarning(Object.freeze({
|
|||
|
|
sessionId: binding.sessionId,
|
|||
|
|
direction: binding.direction,
|
|||
|
|
providerId: binding.providerId,
|
|||
|
|
pluginId: binding.pluginId,
|
|||
|
|
pluginVersion: binding.pluginVersion,
|
|||
|
|
runtimeId: binding.runtimeId,
|
|||
|
|
runtimeKind: binding.runtimeKind,
|
|||
|
|
securityPrincipal: binding.securityPrincipal,
|
|||
|
|
code,
|
|||
|
|
message,
|
|||
|
|
}));
|
|||
|
|
} catch {}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function disable(binding, code, message) {
|
|||
|
|
if (!binding.active) return;
|
|||
|
|
binding.active = false;
|
|||
|
|
bindings.delete(keyOf(binding.sessionId, binding.direction));
|
|||
|
|
refreshOutputMode(binding.sessionId);
|
|||
|
|
binding.removeListener?.();
|
|||
|
|
for (const pending of binding.pending.values()) {
|
|||
|
|
clearTimeout(pending.timer);
|
|||
|
|
pending.resolve(null);
|
|||
|
|
}
|
|||
|
|
binding.pending.clear();
|
|||
|
|
try { binding.port.close?.(); } catch {}
|
|||
|
|
if (!["detached", "replaced", "shutdown", "session-closed"].includes(code)) {
|
|||
|
|
warn(binding, code, message);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function attach(descriptor, port) {
|
|||
|
|
const sessionId = typeof descriptor?.sessionId === "string" ? descriptor.sessionId : "";
|
|||
|
|
const direction = descriptor?.direction;
|
|||
|
|
const identityFields = [
|
|||
|
|
descriptor?.pluginId,
|
|||
|
|
descriptor?.pluginVersion,
|
|||
|
|
descriptor?.runtimeId,
|
|||
|
|
descriptor?.securityPrincipal,
|
|||
|
|
descriptor?.providerId,
|
|||
|
|
];
|
|||
|
|
if (!sessionId || sessionId.length > 256
|
|||
|
|
|| (direction !== "input" && direction !== "output")
|
|||
|
|
|| descriptor?.runtimeKind !== "utility"
|
|||
|
|
|| identityFields.some((value) => typeof value !== "string" || value.length < 1 || value.length > 512)
|
|||
|
|
|| !port?.postMessage) {
|
|||
|
|
throw new TypeError("Terminal interceptor attachment is invalid");
|
|||
|
|
}
|
|||
|
|
const key = keyOf(sessionId, direction);
|
|||
|
|
const previous = bindings.get(key);
|
|||
|
|
const previousRawTail = outputRawTails.get(sessionId);
|
|||
|
|
const wasSensitive = sensitiveInputSessions.has(sessionId);
|
|||
|
|
if (previous) disable(previous, "replaced", "Terminal interceptor was replaced for this session");
|
|||
|
|
const binding = {
|
|||
|
|
sessionId,
|
|||
|
|
direction,
|
|||
|
|
providerId: String(descriptor.providerId ?? ""),
|
|||
|
|
pluginId: String(descriptor.pluginId ?? ""),
|
|||
|
|
pluginVersion: descriptor.pluginVersion,
|
|||
|
|
runtimeId: descriptor.runtimeId,
|
|||
|
|
runtimeKind: descriptor.runtimeKind,
|
|||
|
|
securityPrincipal: descriptor.securityPrincipal,
|
|||
|
|
port,
|
|||
|
|
active: true,
|
|||
|
|
nextSequence: 1,
|
|||
|
|
pending: new Map(),
|
|||
|
|
queuedBytes: 0,
|
|||
|
|
outputExpansionCreditBytes: direction === "output" ? outputWindowBytes : 0,
|
|||
|
|
queue: Promise.resolve(),
|
|||
|
|
removeListener: null,
|
|||
|
|
};
|
|||
|
|
binding.removeListener = addPortListener(port, (event) => {
|
|||
|
|
let envelope;
|
|||
|
|
try {
|
|||
|
|
const message = messageData(event);
|
|||
|
|
envelope = createTerminalInterceptorEnvelope(message?.frame, message?.transfer);
|
|||
|
|
} catch {
|
|||
|
|
disable(binding, "protocol", "Terminal interceptor returned an invalid response and was disabled");
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
const message = envelope.frame;
|
|||
|
|
if (message.type !== "netcatty:terminal-interceptor:result") {
|
|||
|
|
disable(binding, "protocol", "Terminal interceptor returned an invalid response and was disabled");
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
const pending = binding.pending.get(message.sequence);
|
|||
|
|
if (!pending) {
|
|||
|
|
disable(binding, "protocol", "Terminal interceptor returned an unsolicited or duplicate response and was disabled");
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
binding.pending.delete(message.sequence);
|
|||
|
|
clearTimeout(pending.timer);
|
|||
|
|
if (now() >= pending.deadlineAt) {
|
|||
|
|
pending.resolve(null);
|
|||
|
|
disable(binding, "timeout", `Terminal ${binding.direction} interceptor exceeded its ${pending.deadlineMs} ms budget`);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
if (message.status !== "ok" || message.creditBytes !== pending.sentBytes) {
|
|||
|
|
pending.resolve(null);
|
|||
|
|
disable(binding, "protocol", "Terminal interceptor returned an invalid response and was disabled");
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
pending.resolve(new Uint8Array(envelope.transfer));
|
|||
|
|
});
|
|||
|
|
port.on?.("close", () => disable(binding, "closed", "Terminal interceptor stopped and was disabled"));
|
|||
|
|
port.postMessage(createTerminalInterceptorEnvelope({
|
|||
|
|
type: "netcatty:terminal-interceptor:ready",
|
|||
|
|
sessionId,
|
|||
|
|
direction,
|
|||
|
|
windowBytes: direction === "output" ? outputWindowBytes : MAX_CHUNK_BYTES,
|
|||
|
|
}));
|
|||
|
|
bindings.set(key, binding);
|
|||
|
|
refreshOutputMode(sessionId);
|
|||
|
|
if (previousRawTail !== undefined) outputRawTails.set(sessionId, previousRawTail);
|
|||
|
|
if (direction === "input") {
|
|||
|
|
const lastVisibleLine = previousRawTail === undefined
|
|||
|
|
? ""
|
|||
|
|
: (visibleTerminalTail(previousRawTail).split(/[\r\n]/u).at(-1) ?? "");
|
|||
|
|
if (wasSensitive || SENSITIVE_PROMPT.test(lastVisibleLine)) {
|
|||
|
|
sensitiveInputSessions.add(sessionId);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function detach(sessionId, direction, reason = "detached") {
|
|||
|
|
const directions = direction ? [direction] : ["input", "output"];
|
|||
|
|
for (const item of directions) {
|
|||
|
|
const binding = bindings.get(keyOf(sessionId, item));
|
|||
|
|
if (binding) disable(binding, reason, "Terminal interceptor was detached from this session");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function requestChunk(binding, bytes, deadlineAt, deadlineMs) {
|
|||
|
|
if (!binding.active) return Promise.resolve(null);
|
|||
|
|
const remainingMs = deadlineAt - now();
|
|||
|
|
if (remainingMs <= 0) {
|
|||
|
|
disable(binding, "timeout", `Terminal ${binding.direction} interceptor exceeded its ${deadlineMs} ms budget`);
|
|||
|
|
return Promise.resolve(null);
|
|||
|
|
}
|
|||
|
|
const sequence = binding.nextSequence++;
|
|||
|
|
const data = toTransferBuffer(bytes);
|
|||
|
|
return new Promise((resolve) => {
|
|||
|
|
const timer = setTimeout(() => {
|
|||
|
|
binding.pending.delete(sequence);
|
|||
|
|
resolve(null);
|
|||
|
|
disable(binding, "timeout", `Terminal ${binding.direction} interceptor exceeded its ${deadlineMs} ms budget`);
|
|||
|
|
}, remainingMs);
|
|||
|
|
binding.pending.set(sequence, {
|
|||
|
|
resolve,
|
|||
|
|
timer,
|
|||
|
|
deadlineAt,
|
|||
|
|
deadlineMs,
|
|||
|
|
sentBytes: bytes.byteLength,
|
|||
|
|
});
|
|||
|
|
try {
|
|||
|
|
const envelope = createTerminalInterceptorEnvelope({
|
|||
|
|
type: "netcatty:terminal-interceptor:chunk",
|
|||
|
|
sequence,
|
|||
|
|
direction: binding.direction,
|
|||
|
|
creditBytes: binding.direction === "output"
|
|||
|
|
? Math.max(0, outputWindowBytes - binding.queuedBytes)
|
|||
|
|
: MAX_CHUNK_BYTES,
|
|||
|
|
byteLength: data.byteLength,
|
|||
|
|
}, data);
|
|||
|
|
binding.port.postMessage(envelope, [data]);
|
|||
|
|
} catch {
|
|||
|
|
clearTimeout(timer);
|
|||
|
|
binding.pending.delete(sequence);
|
|||
|
|
resolve(null);
|
|||
|
|
disable(binding, "closed", "Terminal interceptor transport failed and was disabled");
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function observeOutput(sessionId, data) {
|
|||
|
|
const mode = outputModes.get(sessionId) ?? 0;
|
|||
|
|
let sensitivePrompt = false;
|
|||
|
|
if (mode !== 0) {
|
|||
|
|
// Retain bounded raw output so an ANSI sequence split across chunks can
|
|||
|
|
// be stripped only after its terminating byte arrives. Persisting the
|
|||
|
|
// already-stripped tail would turn an incomplete CSI into visible text
|
|||
|
|
// and could split a password label such as "Pass\x1b[0" + "mword:".
|
|||
|
|
const rawTail = `${outputRawTails.get(sessionId) ?? ""}${data}`
|
|||
|
|
.slice(-(PROMPT_TAIL_CHARS * 2));
|
|||
|
|
outputRawTails.set(sessionId, rawTail);
|
|||
|
|
const tail = visibleTerminalTail(rawTail);
|
|||
|
|
const lastLine = tail.split(/[\r\n]/u).at(-1) ?? "";
|
|||
|
|
sensitivePrompt = SENSITIVE_PROMPT.test(lastLine);
|
|||
|
|
if ((mode & 1) !== 0) {
|
|||
|
|
if (sensitivePrompt) sensitiveInputSessions.add(sessionId);
|
|||
|
|
else if (CONFIRMED_SHELL_PROMPT.test(lastLine)) sensitiveInputSessions.delete(sessionId);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return (mode & 1) !== 0 ? sensitiveInputSessions.has(sessionId) : sensitivePrompt;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function transform(sessionId, direction, data, options = {}) {
|
|||
|
|
const binding = bindings.get(keyOf(sessionId, direction));
|
|||
|
|
if (!binding?.active) {
|
|||
|
|
return data;
|
|||
|
|
}
|
|||
|
|
const hostSensitive = direction === "input" && sensitiveInputSessions.has(sessionId);
|
|||
|
|
if (options.bypass === true || (direction === "input" && options.sensitive === true) || hostSensitive) {
|
|||
|
|
const finishPassthrough = () => {
|
|||
|
|
if (hostSensitive && /[\r\n]/u.test(String(data))) {
|
|||
|
|
sensitiveInputSessions.delete(sessionId);
|
|||
|
|
outputRawTails.delete(sessionId);
|
|||
|
|
}
|
|||
|
|
return data;
|
|||
|
|
};
|
|||
|
|
const passthrough = binding.queue.then(finishPassthrough, finishPassthrough);
|
|||
|
|
binding.queue = passthrough.then(() => undefined, () => undefined);
|
|||
|
|
return passthrough;
|
|||
|
|
}
|
|||
|
|
const bytes = encoder.encode(String(data));
|
|||
|
|
if (bytes.byteLength === 0) return data;
|
|||
|
|
const deadlineMs = direction === "input" ? inputDeadlineMs : outputDeadlineMs;
|
|||
|
|
const deadlineAt = now() + deadlineMs;
|
|||
|
|
if (direction === "output" && binding.queuedBytes + bytes.byteLength > outputWindowBytes) {
|
|||
|
|
// Chain the fail-open chunk behind all earlier work before disabling the
|
|||
|
|
// binding. disable() releases pending requests, and this queue barrier
|
|||
|
|
// ensures callers cannot deliver the newer chunk first.
|
|||
|
|
const passthrough = binding.queue.then(() => data, () => data);
|
|||
|
|
binding.queue = passthrough.then(() => undefined, () => undefined);
|
|||
|
|
disable(binding, "backpressure", "Terminal output interceptor exceeded its bounded credit window");
|
|||
|
|
return passthrough;
|
|||
|
|
}
|
|||
|
|
binding.queuedBytes += bytes.byteLength;
|
|||
|
|
const run = async () => {
|
|||
|
|
const output = [];
|
|||
|
|
for (let offset = 0; offset < bytes.byteLength;) {
|
|||
|
|
if (!binding.active) return data;
|
|||
|
|
const end = nextUtf8ChunkEnd(bytes, offset);
|
|||
|
|
const chunk = bytes.subarray(offset, end);
|
|||
|
|
offset = end;
|
|||
|
|
const result = await requestChunk(
|
|||
|
|
binding,
|
|||
|
|
chunk,
|
|||
|
|
deadlineAt,
|
|||
|
|
deadlineMs,
|
|||
|
|
);
|
|||
|
|
if (!result) return data;
|
|||
|
|
output.push(result);
|
|||
|
|
offset = end;
|
|||
|
|
}
|
|||
|
|
try {
|
|||
|
|
const total = output.reduce((sum, item) => sum + item.byteLength, 0);
|
|||
|
|
if (direction === "output") {
|
|||
|
|
// Each original byte replenishes one byte of expansion credit. This
|
|||
|
|
// permits a bounded 2x steady-state rewrite plus one initial window
|
|||
|
|
// burst, while repeated tiny-to-64-KiB expansion trips fail-open
|
|||
|
|
// before it can outrun the renderer's display-byte queue.
|
|||
|
|
const replenishedCredit = Math.min(
|
|||
|
|
outputWindowBytes,
|
|||
|
|
binding.outputExpansionCreditBytes + bytes.byteLength,
|
|||
|
|
);
|
|||
|
|
const expansionBytes = Math.max(0, total - bytes.byteLength);
|
|||
|
|
if (total > outputWindowBytes || expansionBytes > replenishedCredit) {
|
|||
|
|
disable(
|
|||
|
|
binding,
|
|||
|
|
"backpressure",
|
|||
|
|
"Terminal output interceptor exceeded its bounded expansion credit",
|
|||
|
|
);
|
|||
|
|
return data;
|
|||
|
|
}
|
|||
|
|
binding.outputExpansionCreditBytes = replenishedCredit - expansionBytes;
|
|||
|
|
}
|
|||
|
|
const combined = new Uint8Array(total);
|
|||
|
|
let offset = 0;
|
|||
|
|
for (const item of output) {
|
|||
|
|
combined.set(item, offset);
|
|||
|
|
offset += item.byteLength;
|
|||
|
|
}
|
|||
|
|
return decoder.decode(combined);
|
|||
|
|
} catch {
|
|||
|
|
disable(binding, "encoding", "Terminal interceptor returned invalid UTF-8 and was disabled");
|
|||
|
|
return data;
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
const result = binding.queue.then(run, run);
|
|||
|
|
binding.queue = result.then(() => undefined, () => undefined);
|
|||
|
|
try { return await result; }
|
|||
|
|
finally { binding.queuedBytes = Math.max(0, binding.queuedBytes - bytes.byteLength); }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return Object.freeze({
|
|||
|
|
attach,
|
|||
|
|
detach,
|
|||
|
|
interceptInput: (sessionId, data, options) => transform(sessionId, "input", data, options),
|
|||
|
|
interceptOutput: (sessionId, data, options) => transform(sessionId, "output", data, options),
|
|||
|
|
has: (sessionId, direction) => bindings.has(keyOf(sessionId, direction)),
|
|||
|
|
getOutputMode: (sessionId) => outputModes.get(sessionId) ?? 0,
|
|||
|
|
observeOutput,
|
|||
|
|
clearSensitiveInput(sessionId) {
|
|||
|
|
sensitiveInputSessions.delete(sessionId);
|
|||
|
|
outputRawTails.delete(sessionId);
|
|||
|
|
},
|
|||
|
|
shutdown() {
|
|||
|
|
for (const binding of [...bindings.values()]) disable(binding, "shutdown", "Terminal interceptor stopped");
|
|||
|
|
},
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
module.exports = {
|
|||
|
|
INPUT_DEADLINE_MS,
|
|||
|
|
MAX_CHUNK_BYTES,
|
|||
|
|
OUTPUT_DEADLINE_MS,
|
|||
|
|
OUTPUT_WINDOW_BYTES,
|
|||
|
|
nextUtf8ChunkEnd,
|
|||
|
|
visibleTerminalTail,
|
|||
|
|
createTerminalDataPipeline,
|
|||
|
|
};
|