refactor: share plugin http dispatch helpers

This commit is contained in:
Vincent Koc
2026-05-29 11:21:02 +02:00
parent 0b84d8b521
commit 49807ac1f1

View File

@@ -6,7 +6,7 @@ import {
} from "../../../packages/gateway-protocol/src/client-info.js";
import { PROTOCOL_VERSION } from "../../../packages/gateway-protocol/src/index.js";
import type { createSubsystemLogger } from "../../logging/subsystem.js";
import type { PluginRegistry } from "../../plugins/registry.js";
import type { PluginHttpRouteRegistration, PluginRegistry } from "../../plugins/registry.js";
import { withPluginRuntimeGatewayRequestScope } from "../../plugins/runtime/gateway-request-scope.js";
import type { AuthorizedGatewayHttpRequest } from "../http-utils.js";
import type { GatewayRequestContext, GatewayRequestOptions } from "../server-methods/types.js";
@@ -30,6 +30,18 @@ export {
export { shouldEnforceGatewayAuthForPluginPath } from "./plugins-http/route-auth.js";
type SubsystemLogger = ReturnType<typeof createSubsystemLogger>;
type PluginRouteRuntimeScope = Parameters<typeof withPluginRuntimeGatewayRequestScope>[0];
function resolvePluginRoutePathContextForRequest(
req: IncomingMessage,
providedPathContext: PluginRoutePathContext | undefined,
): PluginRoutePathContext {
if (providedPathContext) {
return providedPathContext;
}
const url = new URL(req.url ?? "/", "http://localhost");
return resolvePluginRoutePathContext(url.pathname);
}
function createPluginRouteRuntimeClient(
scopes: readonly string[],
@@ -55,6 +67,54 @@ function writeUpgradeUnauthorized(socket: Duplex) {
socket.destroy();
}
type PluginRouteRuntimeDispatchContext = {
gatewayRequestAuth?: AuthorizedGatewayHttpRequest;
gatewayRequestOperatorScopes?: readonly string[];
};
function getMissingPluginRouteRuntimeContext(
route: PluginHttpRouteRegistration,
context: PluginRouteRuntimeDispatchContext,
): "caller auth context" | "caller scope context" | undefined {
if (route.auth !== "gateway") {
return undefined;
}
if (route.gatewayRuntimeScopeSurface === "trusted-operator") {
return context.gatewayRequestAuth ? undefined : "caller auth context";
}
return context.gatewayRequestOperatorScopes === undefined ? "caller scope context" : undefined;
}
function createPluginRouteRuntimeScope(params: {
route: PluginHttpRouteRegistration;
req: IncomingMessage;
gatewayRequestContext?: GatewayRequestContext;
gatewayRequestAuth?: AuthorizedGatewayHttpRequest;
gatewayRequestOperatorScopes?: readonly string[];
}): PluginRouteRuntimeScope {
const runtimeScopes =
params.route.auth !== "gateway"
? []
: params.route.gatewayRuntimeScopeSurface === "trusted-operator"
? resolvePluginRouteRuntimeOperatorScopes(
params.req,
params.gatewayRequestAuth!,
"trusted-operator",
)
: params.gatewayRequestOperatorScopes!;
const runtimeClient = createPluginRouteRuntimeClient(runtimeScopes);
return {
...(params.gatewayRequestContext ? { context: params.gatewayRequestContext } : {}),
client: runtimeClient,
isWebchatConnect: () => false,
...(params.route.pluginId ? { pluginId: params.route.pluginId } : {}),
...(params.route.source ? { pluginSource: params.route.source } : {}),
...(params.route.gatewayMethodDispatchAllowed === true
? { gatewayMethodDispatchAllowed: true }
: {}),
};
}
export type PluginRouteDispatchContext = {
gatewayAuthSatisfied?: boolean;
gatewayRequestAuth?: AuthorizedGatewayHttpRequest;
@@ -91,12 +151,7 @@ export function createGatewayPluginRequestHandler(params: {
return false;
}
const pathContext =
providedPathContext ??
(() => {
const url = new URL(req.url ?? "/", "http://localhost");
return resolvePluginRoutePathContext(url.pathname);
})();
const pathContext = resolvePluginRoutePathContextForRequest(req, providedPathContext);
const matchedRoutes = findMatchingPluginHttpRoutes(registry, pathContext);
if (matchedRoutes.length === 0) {
return false;
@@ -112,53 +167,28 @@ export function createGatewayPluginRequestHandler(params: {
// Fail closed before invoking any handlers when matched gateway routes are
// missing the runtime auth/scope context they require.
for (const route of matchedRoutes) {
if (route.auth !== "gateway") {
continue;
}
if (route.gatewayRuntimeScopeSurface === "trusted-operator") {
if (!gatewayRequestAuth) {
log.warn(
`plugin http route blocked without caller auth context (${pathContext.canonicalPath})`,
);
return false;
}
continue;
}
if (gatewayRequestOperatorScopes === undefined) {
const missingRuntimeContext = getMissingPluginRouteRuntimeContext(route, {
gatewayRequestAuth,
gatewayRequestOperatorScopes,
});
if (missingRuntimeContext) {
log.warn(
`plugin http route blocked without caller scope context (${pathContext.canonicalPath})`,
`plugin http route blocked without ${missingRuntimeContext} (${pathContext.canonicalPath})`,
);
return false;
}
}
for (const route of matchedRoutes) {
let runtimeScopes: readonly string[] = [];
if (route.auth === "gateway") {
if (route.gatewayRuntimeScopeSurface === "trusted-operator") {
runtimeScopes = resolvePluginRouteRuntimeOperatorScopes(
req,
gatewayRequestAuth!,
"trusted-operator",
);
} else {
runtimeScopes = gatewayRequestOperatorScopes!;
}
}
const runtimeClient = createPluginRouteRuntimeClient(runtimeScopes);
try {
const handled = await withPluginRuntimeGatewayRequestScope(
{
...(gatewayRequestContext ? { context: gatewayRequestContext } : {}),
client: runtimeClient,
isWebchatConnect: () => false,
...(route.pluginId ? { pluginId: route.pluginId } : {}),
...(route.source ? { pluginSource: route.source } : {}),
...(route.gatewayMethodDispatchAllowed === true
? { gatewayMethodDispatchAllowed: true }
: {}),
},
createPluginRouteRuntimeScope({
route,
req,
gatewayRequestContext,
gatewayRequestAuth,
gatewayRequestOperatorScopes,
}),
async () => route.handler(req, res),
);
if (handled !== false) {
@@ -193,12 +223,7 @@ export function createGatewayPluginUpgradeHandler(params: {
return false;
}
const pathContext =
providedPathContext ??
(() => {
const url = new URL(req.url ?? "/", "http://localhost");
return resolvePluginRoutePathContext(url.pathname);
})();
const pathContext = resolvePluginRoutePathContextForRequest(req, providedPathContext);
const matchedRoutes = findMatchingPluginHttpRoutes(registry, pathContext).filter(
(route) => typeof route.handleUpgrade === "function",
);
@@ -215,22 +240,13 @@ export function createGatewayPluginUpgradeHandler(params: {
const gatewayRequestOperatorScopes = dispatchContext?.gatewayRequestOperatorScopes;
for (const route of matchedRoutes) {
if (route.auth !== "gateway") {
continue;
}
if (route.gatewayRuntimeScopeSurface === "trusted-operator") {
if (!gatewayRequestAuth) {
log.warn(
`plugin http upgrade blocked without caller auth context (${pathContext.canonicalPath})`,
);
writeUpgradeUnauthorized(socket);
return true;
}
continue;
}
if (gatewayRequestOperatorScopes === undefined) {
const missingRuntimeContext = getMissingPluginRouteRuntimeContext(route, {
gatewayRequestAuth,
gatewayRequestOperatorScopes,
});
if (missingRuntimeContext) {
log.warn(
`plugin http upgrade blocked without caller scope context (${pathContext.canonicalPath})`,
`plugin http upgrade blocked without ${missingRuntimeContext} (${pathContext.canonicalPath})`,
);
writeUpgradeUnauthorized(socket);
return true;
@@ -238,32 +254,15 @@ export function createGatewayPluginUpgradeHandler(params: {
}
for (const route of matchedRoutes) {
let runtimeScopes: readonly string[] = [];
if (route.auth === "gateway") {
if (route.gatewayRuntimeScopeSurface === "trusted-operator") {
runtimeScopes = resolvePluginRouteRuntimeOperatorScopes(
req,
gatewayRequestAuth!,
"trusted-operator",
);
} else {
runtimeScopes = gatewayRequestOperatorScopes!;
}
}
const runtimeClient = createPluginRouteRuntimeClient(runtimeScopes);
try {
const handled = await withPluginRuntimeGatewayRequestScope(
{
...(gatewayRequestContext ? { context: gatewayRequestContext } : {}),
client: runtimeClient,
isWebchatConnect: () => false,
...(route.pluginId ? { pluginId: route.pluginId } : {}),
...(route.source ? { pluginSource: route.source } : {}),
...(route.gatewayMethodDispatchAllowed === true
? { gatewayMethodDispatchAllowed: true }
: {}),
},
createPluginRouteRuntimeScope({
route,
req,
gatewayRequestContext,
gatewayRequestAuth,
gatewayRequestOperatorScopes,
}),
async () => route.handleUpgrade?.(req, socket, head),
);
if (handled !== false) {