Files
workinf_Blender_Wasm/tools/web/server-job-output.mjs
mes123456 380cbed4ff
Some checks are pending
M6 deployable RC / quick (push) Waiting to run
M6 deployable RC / chromium (push) Blocked by required conditions
M6 deployable RC / release (push) Blocked by required conditions
Checkpoint web parity through Chromium input tasks
2026-08-19 10:39:03 -04:00

120 lines
5.0 KiB
JavaScript

export const SERVER_JOB_OUTPUT_SCHEMA = 1;
export const SERVER_JOB_OUTPUT_LIMITS = Object.freeze({
stdoutBytes: 64 * 1024,
stderrBytes: 64 * 1024,
totalBytes: 128 * 1024,
});
const REDACTION = "<redacted>";
const PATH_REDACTION = "<internal-path>";
const TRUNCATION = "\n<output-truncated>";
const CREDENTIAL_KEY = "(?:token|api[_-]?key|secret|password|passwd|authorization|credential|private[_-]?key|access[_-]?key|client[_-]?secret)";
const CREDENTIAL_ASSIGNMENT = /(\b(?:token|api[_-]?key|secret|password|passwd|authorization|credential|private[_-]?key|access[_-]?key|client[_-]?secret)\b\s*[:=]\s*)(["']?)([^\s,;"']+)\2/giu;
const BEARER = /\bBearer\s+[A-Za-z0-9._~+/=-]+/giu;
const BASIC = /\bBasic\s+[A-Za-z0-9+/=]+/giu;
const SECRET_HEADER = /(\b(?:authorization|proxy-authorization)\s*:\s*)([^\r\n]+)/giu;
const UNIX_PATH = /\/(?:[^\s/\\:*?"<>|]+\/)*[^\s/\\:*?"<>|]+/gu;
const WINDOWS_PATH = /\b[A-Za-z]:\\(?:[^\s\\/:*?"<>|]+\\)*[^\s\\/:*?"<>|]*/gu;
const FILE_URL = /\bfile:\/\/[^\s"']+/giu;
function invalid(message) {
throw new Error(`SERVER_JOB_OUTPUT_INVALID: ${message}`);
}
function assertText(value, field) {
if (typeof value !== "string") invalid(`${field} must be a string`);
return value;
}
function assertLimit(value, field) {
if (!Number.isSafeInteger(value) || value < 1 || value > SERVER_JOB_OUTPUT_LIMITS[field]) {
invalid(`${field} is outside the fixed output budget`);
}
return value;
}
function redactCredentials(value) {
let text = value;
let count = 0;
const replace = (pattern, replacement) => {
text = text.replace(pattern, (...args) => {
count += 1;
return typeof replacement === "function" ? replacement(...args) : replacement;
});
};
replace(CREDENTIAL_ASSIGNMENT, (_match, prefix, quote) => `${prefix}${quote}${REDACTION}${quote}`);
replace(BEARER, `Bearer ${REDACTION}`);
replace(BASIC, `Basic ${REDACTION}`);
replace(SECRET_HEADER, (_match, prefix) => `${prefix}${REDACTION}`);
return { text, count };
}
function redactPaths(value) {
let count = 0;
let text = value.replace(FILE_URL, () => { count += 1; return PATH_REDACTION; });
text = text.replace(WINDOWS_PATH, () => { count += 1; return PATH_REDACTION; });
text = text.replace(UNIX_PATH, (match, offset, source) => {
// Keep URL paths and protocol markers readable; only redact filesystem-looking paths.
const before = source.slice(Math.max(0, offset - 8), offset);
if (/https?:$|https?:\/\/$/iu.test(before) || match === "/") return match;
count += 1;
return PATH_REDACTION;
});
return { text, count };
}
function truncateUtf8(value, maxBytes) {
const source = Buffer.from(value, "utf8");
if (source.byteLength <= maxBytes) return { text: value, truncated: false };
const marker = Buffer.from(TRUNCATION, "utf8");
const available = Math.max(0, maxBytes - marker.byteLength);
let end = Math.min(available, source.byteLength);
while (end > 0 && (source[end] & 0xc0) === 0x80) end -= 1;
return { text: `${source.subarray(0, end).toString("utf8")}${TRUNCATION}`, truncated: true };
}
export function sanitizeServerJobOutput(value, maxBytes) {
assertText(value, "output");
assertLimit(maxBytes, "stdoutBytes");
const originalBytes = Buffer.byteLength(value, "utf8");
const credentials = redactCredentials(value);
const paths = redactPaths(credentials.text);
const truncated = truncateUtf8(paths.text, maxBytes);
return Object.freeze({
text: truncated.text,
originalBytes,
emittedBytes: Buffer.byteLength(truncated.text, "utf8"),
redactionCount: credentials.count + paths.count,
truncated: truncated.truncated,
});
}
export function createServerJobOutputReceipt(value, limits = SERVER_JOB_OUTPUT_LIMITS) {
if (!value || typeof value !== "object") invalid("receipt input is invalid");
if (!limits || typeof limits !== "object") invalid("limits are invalid");
const stdoutBytes = assertLimit(limits.stdoutBytes, "stdoutBytes");
const stderrBytes = assertLimit(limits.stderrBytes, "stderrBytes");
const totalBytes = assertLimit(limits.totalBytes, "totalBytes");
if (stdoutBytes + stderrBytes > totalBytes) invalid("stream budgets exceed total budget");
const stdout = sanitizeServerJobOutput(value.stdout ?? "", "stdoutBytes" in limits ? stdoutBytes : stdoutBytes);
const stderr = sanitizeServerJobOutput(value.stderr ?? "", "stderrBytes" in limits ? stderrBytes : stderrBytes);
const totalOriginalBytes = stdout.originalBytes + stderr.originalBytes;
const totalEmittedBytes = stdout.emittedBytes + stderr.emittedBytes;
const totalTruncated = stdout.truncated || stderr.truncated || totalOriginalBytes > totalBytes;
return Object.freeze({
schemaVersion: SERVER_JOB_OUTPUT_SCHEMA,
stdout,
stderr,
limits: Object.freeze({ stdoutBytes, stderrBytes, totalBytes }),
totalOriginalBytes,
totalEmittedBytes,
totalRedactions: stdout.redactionCount + stderr.redactionCount,
totalTruncated,
execution: "DISABLED",
});
}
export function redactServerJobOutput(value, maxBytes) {
return sanitizeServerJobOutput(value, maxBytes).text;
}