release: WRNexusJS 0.2.30

This commit is contained in:
2026-07-14 20:01:16 +05:30
parent e6dbb5b0fc
commit 3cfdc747fa
54 changed files with 155 additions and 96 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/dev-server",
"version": "0.2.29",
"version": "0.2.30",
"type": "module",
"main": "src/index.ts",
"exports": {
+62 -12
View File
@@ -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(