T-HUB/packages/egress/src/client.js
glass beadian aa5e262f3a Tactical Media Broadcast Hub — initial release
Self-hosted multi-platform broadcast newsroom for activist collectives:
publishing pipeline with group approval and DST-proof scheduling, TweetDeck-style
column wall with hostile-content-safe rendering, live control room with Auto
Director, bridged chat governance with polls and quorum, distributed sandboxed
transcode workers, encrypted content-addressed archive, monthly analytics.

MIT licensed. See README.md and docs/.
2026-10-10 11:56:42 +00:00

142 lines
No EOL
7.5 KiB
JavaScript

import { HostBudgets } from "./budget.js";
import { EgressError } from "./errors.js";
import { EgressPolicy } from "./policy.js";
import { requestPinned } from "./transport.js";
const redirectStatuses = new Set([301, 302, 303, 307, 308]);
export function createEgressClient(options) {
const policy = new EgressPolicy(options);
const budgets = new HostBudgets(options);
const timeoutMs = options.timeoutMs ?? 15_000;
const maxResponseBytes = options.maxResponseBytes ?? 8 * 1024 * 1024;
const maxRequestBytes = options.maxRequestBytes ?? 8 * 1024 * 1024;
const maxRedirects = options.maxRedirects ?? 5;
if (![timeoutMs, maxResponseBytes, maxRequestBytes].every((value) => Number.isSafeInteger(value) && value > 0) || !Number.isSafeInteger(maxRedirects) || maxRedirects < 0) {
throw new EgressError("INVALID_CONFIGURATION", "Invalid request limits");
}
async function egressFetch(raw, init = {}, context) {
const controller = new AbortController();
const abort = () => controller.abort(new EgressError("ABORTED", "Request aborted"));
init.signal?.addEventListener("abort", abort, { once: true });
if (init.signal?.aborted)
abort();
const timer = setTimeout(() => controller.abort(new EgressError("TIMEOUT", "Egress deadline exceeded")), timeoutMs);
const check = () => { if (controller.signal.aborted)
throw controller.signal.reason; };
let abortListener;
const cancelled = new Promise((_resolve, reject) => {
abortListener = () => reject(controller.signal.reason);
controller.signal.addEventListener("abort", abortListener, { once: true });
if (controller.signal.aborted)
abortListener();
});
const run = async () => {
check();
let url = policy.validateUrl(raw);
const pinnedOrigin = context.pinnedOrigin === true ? url.origin : context.pinnedOrigin ? policy.validateUrl(context.pinnedOrigin).origin : undefined;
if (pinnedOrigin && url.origin !== pinnedOrigin)
throw new EgressError("ORIGIN_DENIED", "Request is outside the pinned origin");
// Request provides fetch method/header/body normalization; the transport owns connection options.
let normalized;
try {
normalized = new Request(url, init);
}
catch {
throw new EgressError("INVALID_REQUEST", "Invalid fetch request initialization");
}
let method = normalized.method;
const headers = new Headers(normalized.headers);
let body;
if (normalized.body) {
const reader = normalized.body.getReader();
const cancelBody = () => { void reader.cancel().catch(() => undefined); };
controller.signal.addEventListener("abort", cancelBody, { once: true });
const chunks = [];
let size = 0;
try {
for (;;) {
check();
const { value, done } = await reader.read();
if (done)
break;
size += value.length;
if (size > maxRequestBytes)
throw new EgressError("REQUEST_TOO_LARGE", "Request exceeds size limit");
chunks.push(Buffer.from(value));
}
body = Buffer.concat(chunks);
}
finally {
controller.signal.removeEventListener("abort", cancelBody);
void reader.cancel().catch(() => undefined);
}
}
const visited = new Set();
for (let hop = 0;; hop++) {
check();
if (visited.has(url.href))
throw new EgressError("REDIRECT_LOOP", "Redirect cycle detected");
visited.add(url.href);
const endpoint = await policy.validate(url);
check();
await budgets.consume(endpoint.hostKey, context.hostClass);
check();
const wire = await requestPinned(endpoint, method, headers, body, controller.signal, maxResponseBytes, (status, responseHeaders) => budgets.observe(endpoint.hostKey, context.hostClass, status, responseHeaders));
check();
const location = wire.headers.get("location");
if (!redirectStatuses.has(wire.status) || !location || init.redirect === "manual") {
const response = new Response(method === "HEAD" || [204, 205, 304].includes(wire.status) ? null : new Uint8Array(wire.body), { status: wire.status, statusText: wire.statusText, headers: wire.headers });
Object.defineProperties(response, { url: { value: url.href }, redirected: { value: hop > 0 } });
return response;
}
if (init.redirect === "error" || hop >= maxRedirects)
throw new EgressError("REDIRECT_LOOP", "Redirect limit exceeded");
if (/[\s\\\u0000-\u001f\u007f]/u.test(location))
throw new EgressError("INVALID_URL", "Malformed redirect target");
let target;
try {
target = policy.validateUrl(new URL(location, url));
}
catch (error) {
if (error instanceof EgressError)
throw error;
throw new EgressError("INVALID_URL", "Invalid redirect target");
}
if (pinnedOrigin && target.origin !== pinnedOrigin)
throw new EgressError("ORIGIN_DENIED", "Redirect is outside the pinned origin");
if (target.origin !== url.origin) {
for (const name of ["authorization", "proxy-authorization", "cookie", "cookie2"])
headers.delete(name);
// A body can contain secrets just as headers can; refuse to replay it off-origin.
if (body && (wire.status === 307 || wire.status === 308 || method !== "POST" && wire.status !== 303)) {
throw new EgressError("ORIGIN_DENIED", "Request body cannot be redirected off-origin");
}
}
if (wire.status === 303 && method !== "HEAD" || (wire.status === 301 || wire.status === 302) && method === "POST") {
method = "GET";
body = undefined;
for (const name of ["content-type", "content-length", "content-encoding", "content-language", "content-location"])
headers.delete(name);
}
url = target;
}
};
try {
return await Promise.race([run(), cancelled]);
}
catch (error) {
throw error instanceof EgressError ? error : new EgressError("NETWORK_ERROR", "Egress request failed");
}
finally {
clearTimeout(timer);
init.signal?.removeEventListener("abort", abort);
if (abortListener)
controller.signal.removeEventListener("abort", abortListener);
}
}
return { egressFetch, policy, budgets };
}
/** Stateless convenience form. Reuse createEgressClient when pool/config are fixed. */
export function egressFetch(url, init = {}, options) {
return createEgressClient(options).egressFetch(url, init, options);
}
//# sourceMappingURL=client.js.map