diff --git a/src/gateway/server/ws-connection/handshake-auth-helpers.test.ts b/src/gateway/server/ws-connection/handshake-auth-helpers.test.ts index d978de129b3..e5c54542eb9 100644 --- a/src/gateway/server/ws-connection/handshake-auth-helpers.test.ts +++ b/src/gateway/server/ws-connection/handshake-auth-helpers.test.ts @@ -27,6 +27,8 @@ type PairingLocalityOverrides = { sharedAuthOk?: boolean; authMethod?: PairingLocalityParams["authMethod"]; }; +type SilentLocalPairingParams = Parameters[0]; +type BackendSelfPairingParams = Parameters[0]; const CONTROL_UI_WEBCHAT_CONNECT_PARAMS = { client: { @@ -49,6 +51,13 @@ const NODE_HOST_CONNECT_PARAMS = { }, } as ConnectParams; +const CLI_CONNECT_PARAMS = { + client: { + id: GATEWAY_CLIENT_IDS.CLI, + mode: GATEWAY_CLIENT_MODES.CLI, + }, +} as ConnectParams; + function createRateLimiter(): AuthRateLimiter { return { check: () => ({ allowed: true, remaining: 1, retryAfterMs: 0 }), @@ -74,11 +83,15 @@ function resolveDockerPublishedBrowserLocality(overrides: PairingLocalityOverrid }); } -function resolveNodeLoopbackLocality(overrides: PairingLocalityOverrides = {}) { +function resolveLoopbackLocality( + connectParams: ConnectParams, + overrides: PairingLocalityOverrides = {}, + requestHost = "127.0.0.1:18789", +) { return resolvePairingLocality({ - connectParams: overrides.connectParams ?? NODE_HOST_CONNECT_PARAMS, + connectParams: overrides.connectParams ?? connectParams, isLocalClient: overrides.isLocalClient ?? false, - requestHost: overrides.requestHost ?? "127.0.0.1:18789", + requestHost: overrides.requestHost ?? requestHost, requestOrigin: overrides.requestOrigin, remoteAddress: overrides.remoteAddress ?? "127.0.0.1", hasProxyHeaders: overrides.hasProxyHeaders ?? false, @@ -88,6 +101,36 @@ function resolveNodeLoopbackLocality(overrides: PairingLocalityOverrides = {}) { }); } +function resolveNodeLoopbackLocality(overrides: PairingLocalityOverrides = {}) { + return resolveLoopbackLocality(NODE_HOST_CONNECT_PARAMS, overrides); +} + +function resolveCliLoopbackLocality(overrides: PairingLocalityOverrides = {}) { + return resolveLoopbackLocality(CLI_CONNECT_PARAMS, overrides, "172.17.0.2:18789"); +} + +function allowSilentLocalPairing(overrides: Partial) { + return shouldAllowSilentLocalPairing({ + locality: "direct_local", + hasBrowserOriginHeader: false, + isControlUi: false, + isWebchat: false, + reason: "not-paired", + ...overrides, + }); +} + +function skipBackendSelfPairing(overrides: Partial = {}) { + return shouldSkipLocalBackendSelfPairing({ + connectParams: GATEWAY_BACKEND_CONNECT_PARAMS, + locality: "direct_local", + hasBrowserOriginHeader: false, + sharedAuthOk: true, + authMethod: "token", + ...overrides, + }); +} + describe("handshake auth helpers", () => { it("pins browser-origin loopback clients to the synthetic rate-limit ip", () => { const rateLimiter = createRateLimiter(); @@ -160,38 +203,22 @@ describe("handshake auth helpers", () => { it("allows silent local pairing for not-paired, scope-upgrade and role-upgrade", () => { expect( - shouldAllowSilentLocalPairing({ - locality: "direct_local", - hasBrowserOriginHeader: false, - isControlUi: false, - isWebchat: false, + allowSilentLocalPairing({ reason: "not-paired", }), ).toBe(true); expect( - shouldAllowSilentLocalPairing({ - locality: "direct_local", - hasBrowserOriginHeader: false, - isControlUi: false, - isWebchat: false, + allowSilentLocalPairing({ reason: "role-upgrade", }), ).toBe(true); expect( - shouldAllowSilentLocalPairing({ - locality: "direct_local", - hasBrowserOriginHeader: false, - isControlUi: false, - isWebchat: false, + allowSilentLocalPairing({ reason: "scope-upgrade", }), ).toBe(true); expect( - shouldAllowSilentLocalPairing({ - locality: "direct_local", - hasBrowserOriginHeader: false, - isControlUi: false, - isWebchat: false, + allowSilentLocalPairing({ reason: "metadata-upgrade", }), ).toBe(false); @@ -199,7 +226,7 @@ describe("handshake auth helpers", () => { it("allows Control UI or WebChat browser-origin pairing but keeps other browser-origin clients explicit", () => { expect( - shouldAllowSilentLocalPairing({ + allowSilentLocalPairing({ locality: "browser_container_local", hasBrowserOriginHeader: true, isControlUi: true, @@ -208,20 +235,17 @@ describe("handshake auth helpers", () => { }), ).toBe(true); expect( - shouldAllowSilentLocalPairing({ + allowSilentLocalPairing({ locality: "shared_secret_loopback_local", hasBrowserOriginHeader: true, - isControlUi: false, isWebchat: true, reason: "scope-upgrade", }), ).toBe(true); expect( - shouldAllowSilentLocalPairing({ + allowSilentLocalPairing({ locality: "shared_secret_loopback_local", hasBrowserOriginHeader: true, - isControlUi: false, - isWebchat: false, reason: "scope-upgrade", }), ).toBe(false); @@ -229,11 +253,8 @@ describe("handshake auth helpers", () => { it("rejects silent role-upgrade for remote clients", () => { expect( - shouldAllowSilentLocalPairing({ + allowSilentLocalPairing({ locality: "remote", - hasBrowserOriginHeader: false, - isControlUi: false, - isWebchat: false, reason: "role-upgrade", }), ).toBe(false); @@ -242,7 +263,7 @@ describe("handshake auth helpers", () => { it("allows Control UI browser-origin local pairing for fresh pairing and upgrades", () => { for (const locality of ["direct_local", "browser_container_local"] as const) { expect( - shouldAllowSilentLocalPairing({ + allowSilentLocalPairing({ locality, hasBrowserOriginHeader: true, isControlUi: true, @@ -251,7 +272,7 @@ describe("handshake auth helpers", () => { }), ).toBe(true); expect( - shouldAllowSilentLocalPairing({ + allowSilentLocalPairing({ locality, hasBrowserOriginHeader: true, isControlUi: true, @@ -263,15 +284,9 @@ describe("handshake auth helpers", () => { }); it("classifies direct local requests ahead of any Docker CLI fallback", () => { - const connectParams = { - client: { - id: GATEWAY_CLIENT_IDS.CLI, - mode: GATEWAY_CLIENT_MODES.CLI, - }, - } as ConnectParams; expect( resolvePairingLocality({ - connectParams, + connectParams: CLI_CONNECT_PARAMS, isLocalClient: true, requestHost: "gateway.example", remoteAddress: "203.0.113.20", @@ -327,72 +342,28 @@ describe("handshake auth helpers", () => { }); it("classifies CLI loopback/private-host connects as cli_container_local only with shared auth", () => { - const connectParams = { - client: { - id: GATEWAY_CLIENT_IDS.CLI, - mode: GATEWAY_CLIENT_MODES.CLI, - }, - } as ConnectParams; + expect(resolveCliLoopbackLocality()).toBe("cli_container_local"); expect( - resolvePairingLocality({ - connectParams, - isLocalClient: false, - requestHost: "172.17.0.2:18789", - remoteAddress: "127.0.0.1", - hasProxyHeaders: false, - hasBrowserOriginHeader: false, - sharedAuthOk: true, - authMethod: "token", - }), - ).toBe("cli_container_local"); - expect( - resolvePairingLocality({ - connectParams, - isLocalClient: false, - requestHost: "172.17.0.2:18789", - remoteAddress: "127.0.0.1", + resolveCliLoopbackLocality({ hasProxyHeaders: true, - hasBrowserOriginHeader: false, - sharedAuthOk: true, - authMethod: "token", }), ).toBe("remote"); expect( - resolvePairingLocality({ - connectParams, - isLocalClient: false, + resolveCliLoopbackLocality({ requestHost: "gateway.example", - remoteAddress: "127.0.0.1", - hasProxyHeaders: false, - hasBrowserOriginHeader: false, - sharedAuthOk: true, - authMethod: "token", }), ).toBe("remote"); expect( - resolvePairingLocality({ - connectParams, - isLocalClient: false, - requestHost: "172.17.0.2:18789", - remoteAddress: "127.0.0.1", - hasProxyHeaders: false, - hasBrowserOriginHeader: false, - sharedAuthOk: true, + resolveCliLoopbackLocality({ authMethod: "device-token", }), ).toBe("remote"); }); it("classifies non-CLI Docker-published loopback clients as shared_secret_loopback_local when auth is token/password", () => { - const connectParams = { - client: { - id: GATEWAY_CLIENT_IDS.GATEWAY_CLIENT, - mode: GATEWAY_CLIENT_MODES.BACKEND, - }, - } as ConnectParams; expect( resolvePairingLocality({ - connectParams, + connectParams: GATEWAY_BACKEND_CONNECT_PARAMS, isLocalClient: false, requestHost: "172.17.0.2:18789", remoteAddress: "127.0.0.1", @@ -405,164 +376,89 @@ describe("handshake auth helpers", () => { }); it("skips backend self-pairing only for direct-local backend clients", () => { - const connectParams = { - client: { - id: GATEWAY_CLIENT_IDS.GATEWAY_CLIENT, - mode: GATEWAY_CLIENT_MODES.BACKEND, - }, - } as ConnectParams; + expect(skipBackendSelfPairing()).toBe(true); expect( - shouldSkipLocalBackendSelfPairing({ - connectParams, - locality: "direct_local", - hasBrowserOriginHeader: false, - sharedAuthOk: true, - authMethod: "token", - }), - ).toBe(true); - expect( - shouldSkipLocalBackendSelfPairing({ - connectParams, + skipBackendSelfPairing({ locality: "shared_secret_loopback_local", - hasBrowserOriginHeader: false, - sharedAuthOk: true, - authMethod: "token", }), ).toBe(true); expect( - shouldSkipLocalBackendSelfPairing({ - connectParams, + skipBackendSelfPairing({ locality: "remote", - hasBrowserOriginHeader: false, - sharedAuthOk: true, - authMethod: "token", }), ).toBe(false); expect( - shouldSkipLocalBackendSelfPairing({ - connectParams, + skipBackendSelfPairing({ locality: "remote", - hasBrowserOriginHeader: false, - sharedAuthOk: true, authMethod: "password", }), ).toBe(false); expect( - shouldSkipLocalBackendSelfPairing({ - connectParams, - locality: "direct_local", - hasBrowserOriginHeader: false, + skipBackendSelfPairing({ sharedAuthOk: false, authMethod: "device-token", }), ).toBe(true); expect( - shouldSkipLocalBackendSelfPairing({ - connectParams, + skipBackendSelfPairing({ locality: "remote", - hasBrowserOriginHeader: false, sharedAuthOk: false, authMethod: "device-token", }), ).toBe(false); expect( - shouldSkipLocalBackendSelfPairing({ - connectParams, + skipBackendSelfPairing({ locality: "cli_container_local", - hasBrowserOriginHeader: false, - sharedAuthOk: true, - authMethod: "token", }), ).toBe(false); }); it("does not skip backend self-pairing for CLI clients", () => { - const connectParams = { - client: { - id: GATEWAY_CLIENT_IDS.CLI, - mode: GATEWAY_CLIENT_MODES.CLI, - }, - } as ConnectParams; expect( - shouldSkipLocalBackendSelfPairing({ - connectParams, - locality: "direct_local", - hasBrowserOriginHeader: false, - sharedAuthOk: true, - authMethod: "token", + skipBackendSelfPairing({ + connectParams: CLI_CONNECT_PARAMS, }), ).toBe(false); }); it("rejects pairing bypass when browser origin header is present", () => { - const connectParams = { - client: { - id: GATEWAY_CLIENT_IDS.GATEWAY_CLIENT, - mode: GATEWAY_CLIENT_MODES.BACKEND, - }, - } as ConnectParams; expect( - shouldSkipLocalBackendSelfPairing({ - connectParams, - locality: "direct_local", + skipBackendSelfPairing({ hasBrowserOriginHeader: true, - sharedAuthOk: true, - authMethod: "token", }), ).toBe(false); }); it("skips backend self-pairing when auth mode is none (scoped, sharedAuthOk-independent)", () => { - const connectParams = { - client: { - id: GATEWAY_CLIENT_IDS.GATEWAY_CLIENT, - mode: GATEWAY_CLIENT_MODES.BACKEND, - }, - } as ConnectParams; // auth:none on local backend skips regardless of sharedAuthOk expect( - shouldSkipLocalBackendSelfPairing({ - connectParams, - locality: "direct_local", - hasBrowserOriginHeader: false, - sharedAuthOk: true, + skipBackendSelfPairing({ authMethod: "none", }), ).toBe(true); expect( - shouldSkipLocalBackendSelfPairing({ - connectParams, + skipBackendSelfPairing({ locality: "shared_secret_loopback_local", - hasBrowserOriginHeader: false, - sharedAuthOk: true, authMethod: "none", }), ).toBe(true); // sharedAuthOk=false is fine for auth:none on local backend expect( - shouldSkipLocalBackendSelfPairing({ - connectParams, - locality: "direct_local", - hasBrowserOriginHeader: false, + skipBackendSelfPairing({ sharedAuthOk: false, authMethod: "none", }), ).toBe(true); // Remote connections with auth:none should NOT skip expect( - shouldSkipLocalBackendSelfPairing({ - connectParams, + skipBackendSelfPairing({ locality: "remote", - hasBrowserOriginHeader: false, - sharedAuthOk: true, authMethod: "none", }), ).toBe(false); // Browser origin with auth:none should NOT skip expect( - shouldSkipLocalBackendSelfPairing({ - connectParams, - locality: "direct_local", + skipBackendSelfPairing({ hasBrowserOriginHeader: true, sharedAuthOk: false, authMethod: "none", @@ -620,31 +516,22 @@ describe("handshake auth helpers", () => { it("allows silent scope-upgrade, role-upgrade, and metadata-upgrade for shared_secret_loopback_local", () => { expect( - shouldAllowSilentLocalPairing({ + allowSilentLocalPairing({ locality: "shared_secret_loopback_local", - hasBrowserOriginHeader: false, - isControlUi: false, - isWebchat: false, reason: "scope-upgrade", }), ).toBe(true); expect( - shouldAllowSilentLocalPairing({ + allowSilentLocalPairing({ locality: "shared_secret_loopback_local", - hasBrowserOriginHeader: false, - isControlUi: false, - isWebchat: false, reason: "role-upgrade", }), ).toBe(true); // metadata-upgrade now auto-approves for shared_secret_loopback_local // (extended allowlist — see shouldAllowSilentLocalPairing). expect( - shouldAllowSilentLocalPairing({ + allowSilentLocalPairing({ locality: "shared_secret_loopback_local", - hasBrowserOriginHeader: false, - isControlUi: false, - isWebchat: false, reason: "metadata-upgrade", }), ).toBe(true); @@ -653,11 +540,7 @@ describe("handshake auth helpers", () => { describe("shouldAllowSilentLocalPairing — metadata-upgrade reason", () => { it("allows silent metadata-upgrade for direct local native app clients without browser origin", () => { expect( - shouldAllowSilentLocalPairing({ - locality: "direct_local", - hasBrowserOriginHeader: false, - isControlUi: false, - isWebchat: false, + allowSilentLocalPairing({ isNativeAppUi: true, reason: "metadata-upgrade", }), @@ -666,11 +549,7 @@ describe("handshake auth helpers", () => { it("still requires approval for direct local node metadata-upgrade", () => { expect( - shouldAllowSilentLocalPairing({ - locality: "direct_local", - hasBrowserOriginHeader: false, - isControlUi: false, - isWebchat: false, + allowSilentLocalPairing({ reason: "metadata-upgrade", }), ).toBe(false); @@ -678,11 +557,8 @@ describe("handshake auth helpers", () => { it("allows silent metadata-upgrade for cli_container_local CLI clients", () => { expect( - shouldAllowSilentLocalPairing({ + allowSilentLocalPairing({ locality: "cli_container_local", - hasBrowserOriginHeader: false, - isControlUi: false, - isWebchat: false, reason: "metadata-upgrade", }), ).toBe(true); @@ -690,11 +566,8 @@ describe("handshake auth helpers", () => { it("allows silent metadata-upgrade for shared_secret_loopback_local CLI clients", () => { expect( - shouldAllowSilentLocalPairing({ + allowSilentLocalPairing({ locality: "shared_secret_loopback_local", - hasBrowserOriginHeader: false, - isControlUi: false, - isWebchat: false, reason: "metadata-upgrade", }), ).toBe(true); @@ -702,11 +575,8 @@ describe("handshake auth helpers", () => { it("still requires approval for metadata-upgrade from remote clients", () => { expect( - shouldAllowSilentLocalPairing({ + allowSilentLocalPairing({ locality: "remote", - hasBrowserOriginHeader: false, - isControlUi: false, - isWebchat: false, reason: "metadata-upgrade", }), ).toBe(false); @@ -714,7 +584,7 @@ describe("handshake auth helpers", () => { it("still requires approval for metadata-upgrade from browser_container_local (Control UI)", () => { expect( - shouldAllowSilentLocalPairing({ + allowSilentLocalPairing({ locality: "browser_container_local", hasBrowserOriginHeader: true, isControlUi: true, @@ -726,19 +596,15 @@ describe("handshake auth helpers", () => { it("still requires approval for direct local Browser or Control UI metadata-upgrade", () => { expect( - shouldAllowSilentLocalPairing({ - locality: "direct_local", + allowSilentLocalPairing({ hasBrowserOriginHeader: true, isControlUi: true, - isWebchat: false, reason: "metadata-upgrade", }), ).toBe(false); expect( - shouldAllowSilentLocalPairing({ - locality: "direct_local", + allowSilentLocalPairing({ hasBrowserOriginHeader: true, - isControlUi: false, isWebchat: true, reason: "metadata-upgrade", }), @@ -747,22 +613,9 @@ describe("handshake auth helpers", () => { }); it("prefers cli_container_local over shared_secret_loopback_local for CLI clients", () => { - const connectParams = { - client: { - id: GATEWAY_CLIENT_IDS.CLI, - mode: GATEWAY_CLIENT_MODES.CLI, - }, - } as ConnectParams; expect( - resolvePairingLocality({ - connectParams, - isLocalClient: false, + resolveCliLoopbackLocality({ requestHost: "127.0.0.1:18789", - remoteAddress: "127.0.0.1", - hasProxyHeaders: false, - hasBrowserOriginHeader: false, - sharedAuthOk: true, - authMethod: "token", }), ).toBe("cli_container_local"); });