import { env } from "cloudflare:workers";
import { assertPublicHttpsUrl } from "../git/relay";
import { assertResolvesPublic } from "../net/resolve-guard";
import { HttpError, badRequest } from "../errors";
import { isLocalDev } from "../deployment";
const OUTBOUND_TIMEOUT_MS = 10_000;
const MAX_REQUEST_BYTES = 256 * 1024;
const MAX_RESPONSE_BYTES = 64 * 1024;
const HTTP_OK_MIN = 200;
const HTTP_OK_MAX = 300;
const HTTP_REDIRECT_MIN = 300;
const HTTP_REDIRECT_MAX = 400;
const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]"]);
/** `permanent` failures (bad config, rejected URL, most 4xx) are not worth retrying. */
export type OutboundResult = {
status: number | null;
body: string;
error: string | null;
durationMs: number;
permanent?: boolean;
};
/** INTEGRATIONS_ALLOW_LOOPBACK only counts on a loopback forge, like legacy webhooks (DEPLOY-P6). */
export function loopbackSinksAllowed(): boolean {
return env.INTEGRATIONS_ALLOW_LOOPBACK === "true" && isLocalDev();
}
export function isAllowedLoopback(url: URL): boolean {
return loopbackSinksAllowed() && LOOPBACK_HOSTS.has(url.hostname);
}
/**
* Every integration URL must be public https (no IP literals, internal suffixes or our own zone). Local
* development can opt into loopback sinks with INTEGRATIONS_ALLOW_LOOPBACK, which production never sets.
* Legacy repo webhooks may use plain http only when the operator sets WEBHOOKS_ALLOW_HTTP; the host rules
* still apply.
*/
export function assertOutboundUrl(raw: string, allowLoopback = loopbackSinksAllowed(), allowHttp = false): URL {