From 49807ac1f1bd3ccfb9f161eeb7d2c230eabd70e2 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Fri, 29 May 2026 11:21:02 +0200 Subject: [PATCH] refactor: share plugin http dispatch helpers --- src/gateway/server/plugins-http.ts | 179 ++++++++++++++--------------- 1 file changed, 89 insertions(+), 90 deletions(-) diff --git a/src/gateway/server/plugins-http.ts b/src/gateway/server/plugins-http.ts index c78975daca2..aed0f85e26c 100644 --- a/src/gateway/server/plugins-http.ts +++ b/src/gateway/server/plugins-http.ts @@ -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; +type PluginRouteRuntimeScope = Parameters[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) {