release: WRNexusJS 0.2.30
This commit is contained in:
@@ -14,18 +14,27 @@ import { join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { RESTART_EXIT_CODE } from "./restart.ts";
|
||||
|
||||
/** Per-app access control, enforced at the gateway before proxying. */
|
||||
export type GatewayForwardAuth = (
|
||||
| {
|
||||
url: string;
|
||||
app?: never;
|
||||
path?: never;
|
||||
}
|
||||
| {
|
||||
app: string;
|
||||
path?: string;
|
||||
url?: never;
|
||||
}
|
||||
) & {
|
||||
headers?: string[];
|
||||
};
|
||||
|
||||
export interface GatewayAuth {
|
||||
/** HTTP Basic auth — one or more allowed user/password pairs. */
|
||||
basic?: { user: string; pass: string } | Array<{ user: string; pass: string }>;
|
||||
/** Allow only these client IPs (exact match; others get 403). */
|
||||
|
||||
allowIps?: string[];
|
||||
/**
|
||||
* Forward-auth (SSO): the gateway GETs `url` forwarding the request's cookies +
|
||||
* Authorization; a 2xx allows the request, anything else blocks it (its status
|
||||
* is returned). Point it at your own verify endpoint.
|
||||
*/
|
||||
forward?: { url: string };
|
||||
|
||||
forward?: GatewayForwardAuth;
|
||||
}
|
||||
|
||||
export interface GatewayApp {
|
||||
@@ -162,6 +171,7 @@ async function checkAuth(
|
||||
auth: GatewayAuth | undefined,
|
||||
req: Request,
|
||||
ip: string,
|
||||
internalOrigins: Readonly<Record<string, string>>,
|
||||
): Promise<Response | null> {
|
||||
if (!auth) return null;
|
||||
|
||||
@@ -189,12 +199,14 @@ async function checkAuth(
|
||||
}
|
||||
|
||||
if (auth.forward) {
|
||||
const verifyUrl = resolveForwardAuthUrl(auth.forward, internalOrigins);
|
||||
|
||||
try {
|
||||
const res = await fetch(auth.forward.url, {
|
||||
const res = await fetch(verifyUrl, {
|
||||
headers: forwardAuthHeaders(req),
|
||||
redirect: "manual",
|
||||
});
|
||||
if (!res.ok) return forwardAuthFailure(res, auth.forward.url);
|
||||
if (!res.ok) return forwardAuthFailure(res, verifyUrl);
|
||||
} catch {
|
||||
return new Response("Auth service unavailable", { status: 503 });
|
||||
}
|
||||
@@ -203,6 +215,40 @@ async function checkAuth(
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveForwardAuthUrl(
|
||||
forward: NonNullable<GatewayAuth["forward"]>,
|
||||
internalOrigins: Readonly<Record<string, string>>,
|
||||
): string {
|
||||
if (typeof forward.app === "string") {
|
||||
const appName = forward.app;
|
||||
const origin = internalOrigins[appName];
|
||||
|
||||
if (!origin) {
|
||||
throw new Error(`Forward-auth app "${appName}" was not found.`);
|
||||
}
|
||||
|
||||
const path = forward.path ?? "/api/verify";
|
||||
|
||||
if (!path.startsWith("/")) {
|
||||
throw new Error(`Forward-auth path must start with "/": ${path}`);
|
||||
}
|
||||
|
||||
return new URL(path, `${origin}/`).href;
|
||||
}
|
||||
|
||||
if (typeof forward.url === "string") {
|
||||
const url = new URL(forward.url);
|
||||
|
||||
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
||||
throw new Error(`Forward-auth URL must use HTTP or HTTPS: ${forward.url}`);
|
||||
}
|
||||
|
||||
return url.href;
|
||||
}
|
||||
|
||||
throw new Error("Forward auth requires either `app` or `url`.");
|
||||
}
|
||||
|
||||
/** Baseline edge security headers, only where the app didn't already set them. */
|
||||
function applyEdgeHeaders(res: Response): Response {
|
||||
const defaults: Record<string, string> = {
|
||||
@@ -330,6 +376,10 @@ export async function startGateway(opts: GatewayOptions): Promise<RunningGateway
|
||||
return target;
|
||||
});
|
||||
|
||||
const internalOrigins: Readonly<Record<string, string>> = Object.freeze(
|
||||
Object.fromEntries(targets.map((target) => [target.name, target.origin])),
|
||||
);
|
||||
|
||||
const stopChildren = () => {
|
||||
stopping = true;
|
||||
for (const target of targets) {
|
||||
@@ -390,7 +440,7 @@ export async function startGateway(opts: GatewayOptions): Promise<RunningGateway
|
||||
}
|
||||
|
||||
// Per-app access control (basic auth / IP allowlist / forward-auth).
|
||||
const denied = await checkAuth(target.auth, req, ip);
|
||||
const denied = await checkAuth(target.auth, req, ip, internalOrigins);
|
||||
if (denied) {
|
||||
if (sec.accessLog)
|
||||
console.log(
|
||||
|
||||
Reference in New Issue
Block a user