From 298674867416645d9e0821933204de8c91c1a369 Mon Sep 17 00:00:00 2001 From: Ivan Medina Date: Wed, 3 Jun 2026 12:20:35 -0300 Subject: [PATCH 1/6] feat(devtools-connect): emit tunnel error events on the logger COMPASS-8355 Add devtools-connect:tunnel-error and devtools-connect:tunnel-forwarding-error events to ConnectEventMap, wired from the Tunnel object's 'error' and 'forwardingError' events respectively. This allows callers to observe tunnel failures without needing to hold a direct reference to the tunnel object. It is the prerequisite for removing the duplicate SSH tunnel management code in Compass (connect-mongo-client.ts, ssh-tunnel-helpers.ts, data-service.ts) in the next step. --- packages/devtools-connect/src/connect.ts | 10 ++++++++++ packages/devtools-connect/src/types.ts | 7 ++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/devtools-connect/src/connect.ts b/packages/devtools-connect/src/connect.ts index 04705317..1f85e7fd 100644 --- a/packages/devtools-connect/src/connect.ts +++ b/packages/devtools-connect/src/connect.ts @@ -555,6 +555,16 @@ async function connectMongoClientImpl({ 'mongodb://', ); cleanupOnClientClose.push(() => tunnel?.close()); + if (tunnel) { + tunnel.on('error', (err) => { + logger.emit('devtools-connect:tunnel-error', { error: err }); + }); + tunnel.on('forwardingError', (err) => { + logger.emit('devtools-connect:tunnel-forwarding-error', { + error: err, + }); + }); + } } for (const proxyLogger of new Set([ tunnel?.logger, diff --git a/packages/devtools-connect/src/types.ts b/packages/devtools-connect/src/types.ts index 0e2b9d93..58039864 100644 --- a/packages/devtools-connect/src/types.ts +++ b/packages/devtools-connect/src/types.ts @@ -66,7 +66,8 @@ export interface ConnectRetryAfterTLSErrorEvent { } export interface ConnectEventMap - extends MongoDBOIDCLogEventsMap, ProxyEventMap { + extends MongoDBOIDCLogEventsMap, + ProxyEventMap { /** Signals that a connection attempt is about to be performed. */ 'devtools-connect:connect-attempt-initialized': ( ev: ConnectAttemptInitializedEvent, @@ -103,6 +104,10 @@ export interface ConnectEventMap 'devtools-connect:retry-after-tls-error': ( ev: ConnectRetryAfterTLSErrorEvent, ) => void; + /** Signals that the proxy tunnel encountered a session-level error (e.g. SSH connection lost) */ + 'devtools-connect:tunnel-error': (ev: { error: Error }) => void; + /** Signals that the proxy tunnel failed to forward a connection */ + 'devtools-connect:tunnel-forwarding-error': (ev: { error: Error }) => void; } export type ConnectEventArgs = From e5ad2026efe41b4409cff1d29b805145d7245ef8 Mon Sep 17 00:00:00 2001 From: Ivan Medina Date: Wed, 3 Jun 2026 12:27:00 -0300 Subject: [PATCH 2/6] Update packages/devtools-connect/src/connect.ts Co-authored-by: Anna Henningsen --- packages/devtools-connect/src/connect.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/devtools-connect/src/connect.ts b/packages/devtools-connect/src/connect.ts index 1f85e7fd..9ec14d6e 100644 --- a/packages/devtools-connect/src/connect.ts +++ b/packages/devtools-connect/src/connect.ts @@ -555,16 +555,14 @@ async function connectMongoClientImpl({ 'mongodb://', ); cleanupOnClientClose.push(() => tunnel?.close()); - if (tunnel) { - tunnel.on('error', (err) => { - logger.emit('devtools-connect:tunnel-error', { error: err }); - }); - tunnel.on('forwardingError', (err) => { - logger.emit('devtools-connect:tunnel-forwarding-error', { - error: err, - }); + tunnel?.on('error', (err) => { + logger.emit('devtools-connect:tunnel-error', { error: err }); + }); + tunnel?.on('forwardingError', (err) => { + logger.emit('devtools-connect:tunnel-forwarding-error', { + error: err, }); - } + }); } for (const proxyLogger of new Set([ tunnel?.logger, From c110176ce62ce66e25e59a56e743e7e03bcec6a4 Mon Sep 17 00:00:00 2001 From: Ivan Medina Date: Wed, 3 Jun 2026 12:44:39 -0300 Subject: [PATCH 3/6] chore(devtools-connect): fix pre-existing prettier formatting in log-hook.ts COMPASS-8355 --- packages/devtools-connect/src/log-hook.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/devtools-connect/src/log-hook.ts b/packages/devtools-connect/src/log-hook.ts index ee8eefde..753c61ce 100644 --- a/packages/devtools-connect/src/log-hook.ts +++ b/packages/devtools-connect/src/log-hook.ts @@ -21,7 +21,8 @@ import { } from '@mongodb-js/devtools-proxy-support'; export interface MongoLogWriter - extends OIDCMongoLogWriter, ProxyMongoLogWriter {} + extends OIDCMongoLogWriter, + ProxyMongoLogWriter {} export function hookLogger( emitter: ConnectLogEmitter, From 77f5a9ad8d9467a7a81166cd2976a55093eb98a4 Mon Sep 17 00:00:00 2001 From: Ivan Medina Date: Wed, 3 Jun 2026 13:23:56 -0300 Subject: [PATCH 4/6] chore(devtools-connect): reformat with prettier 3.8.3 COMPASS-8355 From e04196b57031fbaff1059ec806bd92813a4006f7 Mon Sep 17 00:00:00 2001 From: Ivan Medina Date: Wed, 3 Jun 2026 13:49:09 -0300 Subject: [PATCH 5/6] chore(devtools-connect): fix formatting --- packages/devtools-connect/src/log-hook.ts | 3 +-- packages/devtools-connect/src/types.ts | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/devtools-connect/src/log-hook.ts b/packages/devtools-connect/src/log-hook.ts index 753c61ce..ee8eefde 100644 --- a/packages/devtools-connect/src/log-hook.ts +++ b/packages/devtools-connect/src/log-hook.ts @@ -21,8 +21,7 @@ import { } from '@mongodb-js/devtools-proxy-support'; export interface MongoLogWriter - extends OIDCMongoLogWriter, - ProxyMongoLogWriter {} + extends OIDCMongoLogWriter, ProxyMongoLogWriter {} export function hookLogger( emitter: ConnectLogEmitter, diff --git a/packages/devtools-connect/src/types.ts b/packages/devtools-connect/src/types.ts index 58039864..9c942fa7 100644 --- a/packages/devtools-connect/src/types.ts +++ b/packages/devtools-connect/src/types.ts @@ -66,8 +66,7 @@ export interface ConnectRetryAfterTLSErrorEvent { } export interface ConnectEventMap - extends MongoDBOIDCLogEventsMap, - ProxyEventMap { + extends MongoDBOIDCLogEventsMap, ProxyEventMap { /** Signals that a connection attempt is about to be performed. */ 'devtools-connect:connect-attempt-initialized': ( ev: ConnectAttemptInitializedEvent, From d833ac719381b04060c4acd3523771e36e7d812b Mon Sep 17 00:00:00 2001 From: Ivan Medina Date: Wed, 3 Jun 2026 15:35:32 -0300 Subject: [PATCH 6/6] test(devtools-connect): add coverage for tunnel error event emission COMPASS-8355 Use Object.defineProperty to replace the getter-based createSocks5Tunnel export (sinon.stub cannot replace non-writable getters) with a fake tunnel, then verify that tunnel 'error' and 'forwardingError' events are forwarded as devtools-connect:tunnel-error and devtools-connect:tunnel-forwarding-error on the logger. --- packages/devtools-connect/src/connect.spec.ts | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/packages/devtools-connect/src/connect.spec.ts b/packages/devtools-connect/src/connect.spec.ts index 7adf1e0c..91a65b0a 100644 --- a/packages/devtools-connect/src/connect.spec.ts +++ b/packages/devtools-connect/src/connect.spec.ts @@ -10,6 +10,7 @@ import sinonChai from 'sinon-chai'; import { Agent as HTTPSAgent } from 'https'; import { MongoCluster } from 'mongodb-runner'; import { tmpdir } from 'os'; +import * as devtoolsProxySupport from '@mongodb-js/devtools-proxy-support'; chai.use(sinonChai); @@ -530,6 +531,79 @@ describe('devtools connect', function () { ); }); }); + + describe('tunnel error events', function () { + let originalDescriptor: PropertyDescriptor; + let fakeTunnel: EventEmitter & { + listen: sinon.SinonStub; + close: sinon.SinonStub; + config: any; + logger: EventEmitter; + }; + + beforeEach(function () { + const tunnel = Object.assign(new EventEmitter(), { + listen: sinon.stub().resolves(), + close: sinon.stub().resolves(), + config: { + proxyHost: '127.0.0.1', + proxyPort: 1080, + proxyUsername: 'u', + proxyPassword: 'p', + }, + logger: new EventEmitter(), + }); + fakeTunnel = tunnel as any; + + // devtools-proxy-support exports createSocks5Tunnel as a non-writable + // getter, so sinon.stub() can't replace it. Use Object.defineProperty + // directly to inject the fake tunnel factory. + originalDescriptor = Object.getOwnPropertyDescriptor( + devtoolsProxySupport, + 'createSocks5Tunnel', + )!; + Object.defineProperty(devtoolsProxySupport, 'createSocks5Tunnel', { + configurable: true, + writable: true, + value: () => fakeTunnel, + }); + }); + + afterEach(function () { + Object.defineProperty( + devtoolsProxySupport, + 'createSocks5Tunnel', + originalDescriptor, + ); + }); + + for (const [tunnelEvent, busEvent] of [ + ['error', 'devtools-connect:tunnel-error'], + ['forwardingError', 'devtools-connect:tunnel-forwarding-error'], + ] as const) { + it(`emits ${busEvent} when tunnel emits '${tunnelEvent}'`, async function () { + const mClient = stubConstructor(FakeMongoClient); + const mClientType = sinon.stub().returns(mClient); + mClient.connect.resolves(mClient); + + const { client } = await connectMongoClient( + 'localhost:27017', + { ...defaultOpts, proxy: { proxy: 'socks5://localhost:1080' } }, + bus, + mClientType as any, + ); + + const received: any[] = []; + bus.on(busEvent, (ev) => received.push(ev)); + fakeTunnel.emit(tunnelEvent, new Error(`test ${tunnelEvent}`)); + + expect(received).to.have.lengthOf(1); + expect(received[0].error.message).to.equal(`test ${tunnelEvent}`); + + mClient.emit('close'); + }); + } + }); }); describe('integration', function () {