• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mongodb-js / devtools-shared / 16367244197

18 Jul 2025 09:32AM UTC coverage: 72.518%. First build
16367244197

Pull #564

github

lerouxb
vectorEmbedding index types are apparently not a thing
Pull Request #564: fix(constants): vectorEmbedding index types are apparently not a thing COMPASS-9574

1508 of 2369 branches covered (63.66%)

Branch coverage included in aggregate %.

3255 of 4199 relevant lines covered (77.52%)

594.54 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

33.5
/packages/devtools-connect/src/connect.ts
1
import type { ConnectLogEmitter } from './index';
2
import dns from 'dns';
1!
3
import {
1!
4
  isFastFailureConnectionError,
×
5
  isPotentialTLSCertificateError,
×
6
} from './fast-failure-connect';
×
7
import type {
8
  MongoClient,
×
9
  MongoClientOptions,
10
  ServerHeartbeatFailedEvent,
×
11
  ServerHeartbeatSucceededEvent,
×
12
  TopologyDescription,
13
} from 'mongodb';
1!
14
import type { ConnectDnsResolutionDetail } from './types';
×
15
import type {
16
  MongoDBOIDCPlugin,
×
17
  MongoDBOIDCPluginOptions,
18
} from '@mongodb-js/oidc-plugin';
1✔
19
import { createMongoDBOIDCPlugin } from '@mongodb-js/oidc-plugin';
2!
20
import merge from 'lodash.merge';
1!
21
import { oidcServerRequestHandler } from './oidc/handler';
1✔
22
import { StateShareClient, StateShareServer } from './ipc-rpc-state-share';
1!
23
import ConnectionString, {
2✔
24
  CommaAndColonSeparatedRecord,
25
} from 'mongodb-connection-string-url';
×
26
import { EventEmitter } from 'events';
1✔
27
import {
2✔
28
  createSocks5Tunnel,
1!
29
  DevtoolsProxyOptions,
×
30
  AgentWithInitialize,
×
31
  useOrCreateAgent,
×
32
  Tunnel,
×
33
  systemCA,
34
  createFetch,
35
} from '@mongodb-js/devtools-proxy-support';
1✔
36
export type { DevtoolsProxyOptions, AgentWithInitialize };
2!
37

38
function isAtlas(str: string): boolean {
1✔
39
  try {
1✔
40
    const { hosts } = new ConnectionString(str);
1✔
41
    return hosts.every((host) => /(^|\.)mongodb.net(:|$)/.test(host));
1✔
42
  } catch {
1✔
43
    return false;
1✔
44
  }
1✔
45
}
1✔
46

1✔
47
export class MongoAutoencryptionUnavailable extends Error {
1✔
48
  constructor() {
1✔
49
    super(
1✔
50
      'Automatic encryption is only available with Atlas and MongoDB Enterprise',
1✔
51
    );
52
  }
1✔
53
}
1✔
54

1✔
55
/**
56
 * Takes an unconnected MongoClient and connects it, but fails fast for certain
57
 * errors.
×
58
 */
59
async function connectWithFailFast(
60
  uri: string,
61
  client: MongoClient,
62
  logger: ConnectLogEmitter,
4✔
63
): Promise<void> {
64
  const failedConnections = new Map<string, Error>();
×
65
  let failEarlyClosePromise: Promise<void> | null = null;
1✔
66
  logger.emit('devtools-connect:connect-attempt-initialized', {
×
67
    uri,
16✔
68
    driver: client.options.metadata.driver,
16✔
69
    // eslint-disable-next-line @typescript-eslint/no-var-requires
16✔
70
    devtoolsConnectVersion: require('../package.json').version,
71
    host: client.options.srvHost ?? client.options.hosts.join(','),
1!
72
  });
73

32✔
74
  const heartbeatFailureListener = ({
×
75
    failure,
16✔
76
    connectionId,
4✔
77
  }: ServerHeartbeatFailedEvent) => {
78
    const topologyDescription: TopologyDescription | undefined = (client as any)
4✔
79
      .topology?.description;
4✔
80
    const servers = topologyDescription?.servers;
4✔
81
    const isFailFast = isFastFailureConnectionError(failure);
4✔
82
    const isKnownServer = !!servers?.has(connectionId);
×
83
    logger.emit('devtools-connect:connect-heartbeat-failure', {
×
84
      connectionId,
85
      failure,
×
86
      isFailFast,
×
87
      isKnownServer,
4!
88
    });
×
89
    if (!isKnownServer) {
×
90
      return;
4✔
91
    }
3✔
92

3!
93
    if (isFailFast && servers) {
3!
94
      failedConnections.set(connectionId, failure);
3✔
95
      if (
×
96
        [...servers.keys()].every((server) => failedConnections.has(server))
×
97
      ) {
98
        logger.emit('devtools-connect:connect-fail-early');
16✔
99
        // Setting this variable indicates that we are failing early.
×
100
        failEarlyClosePromise = client.close();
×
101
      }
×
102
    }
×
103
  };
104

16✔
105
  const heartbeatSucceededListener = ({
16!
106
    connectionId,
16✔
107
  }: ServerHeartbeatSucceededEvent) => {
16✔
108
    logger.emit('devtools-connect:connect-heartbeat-succeeded', {
×
109
      connectionId,
×
110
    });
4✔
111
    failedConnections.delete(connectionId);
4✔
112
  };
3✔
113

3✔
114
  client.addListener('serverHeartbeatFailed', heartbeatFailureListener);
×
115
  client.addListener('serverHeartbeatSucceeded', heartbeatSucceededListener);
4✔
116
  try {
×
117
    await client.connect();
×
118
  } catch (error: unknown) {
1✔
119
    let connectErr = error;
×
120
    if (failEarlyClosePromise !== null) {
4!
121
      await (failEarlyClosePromise as Promise<void>);
×
122
      connectErr = failedConnections.values().next().value; // Just use the first failure.
×
123
    }
16✔
124
    if (
16!
125
      typeof connectErr === 'object' &&
16!
126
      connectErr?.constructor.name === 'MongoServerSelectionError' &&
127
      isAtlas(uri)
128
    ) {
129
      (connectErr as Error).message = `${
×
130
        (connectErr as Error).message
131
      }. It looks like this is a MongoDB Atlas cluster. Please ensure that your Network Access List allows connections from your IP.`;
132
    }
×
133
    throw connectErr;
10✔
134
  } finally {
10!
135
    client.removeListener('serverHeartbeatFailed', heartbeatFailureListener);
×
136
    client.removeListener(
×
137
      'serverHeartbeatSucceeded',
138
      heartbeatSucceededListener,
139
    );
140
    logger.emit('devtools-connect:connect-attempt-finished', {
×
141
      cryptSharedLibVersionInfo: (client as any)?.autoEncrypter
×
142
        ?.cryptSharedLibVersionInfo,
×
143
    });
×
144
  }
145
}
146

147
let resolveDnsHelpers:
×
148
  | {
149
      // eslint-disable-next-line @typescript-eslint/consistent-type-imports
150
      resolve: typeof import('resolve-mongodb-srv');
×
151
      // eslint-disable-next-line @typescript-eslint/consistent-type-imports
×
152
      osDns: typeof import('os-dns-native');
×
153
    }
×
154
  | undefined;
×
155

156
async function resolveMongodbSrv(
×
157
  uri: string,
×
158
  logger: ConnectLogEmitter,
×
159
): Promise<string> {
×
160
  const resolutionDetails: ConnectDnsResolutionDetail[] = [];
×
161
  if (uri.startsWith('mongodb+srv://')) {
×
162
    try {
×
163
      resolveDnsHelpers ??= {
×
164
        resolve: require('resolve-mongodb-srv'),
165
        osDns: require('os-dns-native'),
166
      };
×
167
    } catch (error: any) {
×
168
      logger.emit('devtools-connect:resolve-srv-error', {
×
169
        from: '',
×
170
        error,
×
171
        duringLoad: true,
×
172
        resolutionDetails,
×
173
        durationMs: null,
174
      });
175
    }
176
    if (resolveDnsHelpers !== undefined) {
×
177
      const dnsResolutionStart = Date.now();
×
178
      try {
×
179
        const {
×
180
          wasNativelyLookedUp,
181
          withNodeFallback: { resolveSrv, resolveTxt },
182
        } = resolveDnsHelpers.osDns;
×
183
        const resolved = await resolveDnsHelpers.resolve(uri, {
×
184
          dns: {
×
185
            resolveSrv(hostname: string, cb: Parameters<typeof resolveSrv>[1]) {
186
              const start = Date.now();
×
187
              resolveSrv(
×
188
                hostname,
189
                (...args: Parameters<Parameters<typeof resolveSrv>[1]>) => {
190
                  resolutionDetails.push({
×
191
                    query: 'SRV',
×
192
                    hostname,
×
193
                    error: args[0]?.message,
×
194
                    wasNativelyLookedUp: wasNativelyLookedUp(args[1]),
195
                    durationMs: Date.now() - start,
196
                  });
×
197
                  cb(...args);
×
198
                },
199
              );
×
200
            },
×
201
            resolveTxt(hostname: string, cb: Parameters<typeof resolveTxt>[1]) {
202
              resolveTxt(
×
203
                hostname,
204
                (...args: Parameters<Parameters<typeof resolveTxt>[1]>) => {
10✔
205
                  const start = Date.now();
×
206
                  resolutionDetails.push({
×
207
                    query: 'TXT',
13✔
208
                    hostname,
13✔
209
                    error: args[0]?.message,
210
                    wasNativelyLookedUp: wasNativelyLookedUp(args[1]),
211
                    durationMs: Date.now() - start,
×
212
                  });
×
213
                  cb(...args);
×
214
                },
215
              );
×
216
            },
13✔
217
          },
13✔
218
        });
219
        logger.emit('devtools-connect:resolve-srv-succeeded', {
×
220
          from: uri,
×
221
          to: resolved,
222
          resolutionDetails,
×
223
          durationMs: Date.now() - dnsResolutionStart,
224
        });
225
        return resolved;
13✔
226
      } catch (error: any) {
13✔
227
        logger.emit('devtools-connect:resolve-srv-error', {
×
228
          from: uri,
229
          error,
×
230
          duringLoad: false,
231
          resolutionDetails,
232
          durationMs: Date.now() - dnsResolutionStart,
×
233
        });
234
        throw error;
13✔
235
      }
13✔
236
    }
237
  }
238
  return uri;
×
239
}
240

241
function detectAndLogMissingOptionalDependencies(logger: ConnectLogEmitter) {
242
  // These need to be literal require('string') calls for bundling purposes.
243
  try {
13✔
244
    require('socks');
13✔
245
  } catch (error: any) {
246
    logger.emit('devtools-connect:missing-optional-dependency', {
×
247
      name: 'socks',
×
248
      error,
249
    });
250
  }
×
251
  try {
×
252
    require('mongodb-client-encryption');
×
253
  } catch (error: any) {
×
254
    logger.emit('devtools-connect:missing-optional-dependency', {
14✔
255
      name: 'mongodb-client-encryption',
×
256
      error,
×
257
    });
258
  }
×
259
  try {
×
260
    require('os-dns-native');
×
261
  } catch (error: any) {
14✔
262
    logger.emit('devtools-connect:missing-optional-dependency', {
14✔
263
      name: 'os-dns-native',
14✔
264
      error,
14!
265
    });
×
266
  }
×
267
  try {
×
268
    require('resolve-mongodb-srv');
×
269
  } catch (error: any) {
14✔
270
    logger.emit('devtools-connect:missing-optional-dependency', {
14✔
271
      name: 'resolve-mongodb-srv',
14✔
272
      error,
14✔
273
    });
274
  }
×
275
  try {
×
276
    require('kerberos');
×
277
  } catch (error: any) {
×
278
    logger.emit('devtools-connect:missing-optional-dependency', {
×
279
      name: 'kerberos',
280
      error,
×
281
    });
×
282
  }
×
283
}
×
284

×
285
// Override 'from.emit' so that all events also end up being emitted on 'to'
×
286
function copyEventEmitterEvents<M>(
287
  from: {
288
    emit: <K extends string & keyof M>(
1✔
289
      event: K,
290
      ...args: M[K] extends (...args: infer P) => any ? P : never
13✔
291
    ) => void;
13✔
292
  },
13✔
293
  to: {
13✔
294
    emit: <K extends string & keyof M>(
295
      event: K,
296
      ...args: M[K] extends (...args: infer P) => any ? P : never
7✔
297
    ) => void;
1✔
298
  },
299
) {
300
  from.emit = function <K extends string & keyof M>(
1✔
301
    event: K,
1✔
302
    ...args: M[K] extends (...args: infer P) => any ? P : never
303
  ) {
304
    to.emit(event, ...args);
×
305
    return EventEmitter.prototype.emit.call(this, event, ...args);
7✔
306
  };
307
}
308

309
// Wrapper for all state that a devtools application may want to share
14✔
310
// between MongoClient instances. Currently, this is only the OIDC state.
14✔
311
// There are two ways of sharing this state:
312
// - When re-used within the same process/address space, it can be passed
11✔
313
//   to `connectMongoClient()` as `parentState` directly.
8✔
314
// - When re-used across processes, an RPC server can be used over an IPC
315
//   channel by calling `.getStateShareServer()`, which returns a string
316
//   that can then be passed to `connectMongoClient()` as `parentHandle`
14✔
317
//   and which should be considered secret since it contains auth information
14✔
318
//   for that RPC server.
13✔
319
export class DevtoolsConnectionState {
1!
320
  public oidcPlugin: MongoDBOIDCPlugin;
26✔
321
  public productName: string;
322

13✔
323
  private stateShareClient: StateShareClient | null = null;
×
324
  private stateShareServer: StateShareServer | null = null;
×
325

326
  constructor(
327
    options: Pick<
328
      DevtoolsConnectOptions,
13✔
329
      'productDocsLink' | 'productName' | 'oidc' | 'parentHandle'
330
    >,
14!
331
    logger: ConnectLogEmitter,
×
332
  ) {
333
    this.productName = options.productName;
×
334
    if (options.parentHandle) {
×
335
      this.stateShareClient = new StateShareClient(options.parentHandle);
×
336
      this.oidcPlugin = this.stateShareClient.oidcPlugin;
×
337
    } else {
14✔
338
      // Create a separate logger instance for the plugin and "copy" events over
339
      // to the main logger instance, so that when we attach listeners to the plugins,
14!
340
      // they are only triggered for events from that specific plugin instance
×
341
      // (and not other OIDCPlugin instances that might be running on the same logger).
342
      const proxyingLogger = new EventEmitter();
14!
343
      proxyingLogger.setMaxListeners(Infinity);
×
344
      copyEventEmitterEvents(proxyingLogger, logger);
×
345
      this.oidcPlugin = createMongoDBOIDCPlugin({
14!
346
        ...options.oidc,
13✔
347
        logger: proxyingLogger,
×
348
        redirectServerRequestHandler: oidcServerRequestHandler.bind(
14✔
349
          null,
13✔
350
          options,
351
        ),
352
      });
353
    }
×
354
  }
×
355

14!
356
  async getStateShareServer(): Promise<string> {
×
357
    this.stateShareServer ??= await StateShareServer.create(this);
×
358
    return this.stateShareServer.handle;
×
359
  }
14✔
360

14!
361
  async destroy(): Promise<void> {
×
362
    await this.stateShareServer?.close();
×
363
    await this.oidcPlugin?.destroy();
×
364
  }
14!
365
}
×
366

×
367
export interface DevtoolsConnectOptions extends MongoClientOptions {
368
  /**
369
   * An URL that refers to the documentation for the current product.
370
   */
371
  productDocsLink: string;
14✔
372
  /**
14✔
373
   * A human-readable name for the current product (e.g. "MongoDB Compass").
374
   */
14!
375
  productName: string;
14✔
376
  /**
1✔
377
   * A set of options to pass when creating the OIDC plugin. Ignored if `parentState` is set.
378
   */
14✔
379
  oidc?: Omit<
14✔
380
    MongoDBOIDCPluginOptions,
14✔
381
    'logger' | 'redirectServerRequestHandler'
14✔
382
  >;
14✔
383
  /**
14✔
384
   * A `DevtoolsConnectionState` object that refers to the state resulting from another
14✔
385
   * `connectMongoClient()` call.
14✔
386
   */
14✔
387
  parentState?: DevtoolsConnectionState;
388
  /**
389
   * Similar to `parentState`, an opaque handle returned from `createShareStateServer()`
6✔
390
   * may be used to share state from another `DevtoolsConnectionState` instance, possibly
6✔
391
   * residing in another process. This handle should generally be considered a secret.
6✔
392
   *
6✔
393
   * In this case, the application needs to ensure that the lifetime of the top-level state
6✔
394
   * extends beyond the lifetime(s) of the respective dependent state instance(s).
6✔
395
   */
6✔
396
  parentHandle?: string;
397
  /**
398
   * Proxy options or an existing proxy Agent instance that can be shared. These are applied to
399
   * both database cluster traffic and, optionally, OIDC HTTP traffic.
6✔
400
   */
6✔
401
  proxy?: DevtoolsProxyOptions | AgentWithInitialize;
402
  /**
4✔
403
   * Whether the proxy specified in `.proxy` should be applied to OIDC HTTP traffic as well.
404
   * This can be an agent or proxy options object, in which case they will override the ones
405
   * provided in `proxy` for OIDC traffic.
10✔
406
   */
10✔
407
  applyProxyToOIDC?: boolean | DevtoolsProxyOptions | AgentWithInitialize;
10✔
408
}
10✔
409

10✔
410
export type ConnectMongoClientResult = {
6!
411
  client: MongoClient;
×
412
  state: DevtoolsConnectionState;
413
};
6✔
414

415
/**
416
 * Connect a MongoClient. If AutoEncryption is requested, first connect without the encryption options and verify that
8✔
417
 * the connection is to an enterprise cluster. If not, then error, otherwise close the connection and reconnect with the
8✔
418
 * options the user initially specified. Provide the client class as an additional argument in order to test.
419
 */
420
export async function connectMongoClient(
1✔
421
  uri: string,
30✔
422
  clientOptions: DevtoolsConnectOptions,
30✔
423
  logger: ConnectLogEmitter,
424
  MongoClientClass: typeof MongoClient,
425
): Promise<ConnectMongoClientResult> {
25✔
426
  detectAndLogMissingOptionalDependencies(logger);
×
427

428
  const options = { uri, clientOptions, logger, MongoClientClass };
×
429
  // Connect once with the system certificate store added, and if that fails with
13✔
430
  // a TLS error, try again. In theory adding certificates into the certificate store
431
  // should not cause failures, but in practice we have observed some, hence this
432
  // double connection establishment logic.
433
  // We treat TLS errors as fail-fast errors, so in typical situations (even typical
434
  // failure situations) we do not spend an unreasonable amount of time in the first
×
435
  // connection attempt.
×
436
  try {
×
437
    return await connectMongoClientImpl({ ...options, useSystemCA: true });
×
438
  } catch (error: unknown) {
439
    if (isPotentialTLSCertificateError(error)) {
1!
440
      logger.emit('devtools-connect:retry-after-tls-error', {
19!
441
        error: String(error),
×
442
      });
×
443
      try {
×
444
        return await connectMongoClientImpl({ ...options, useSystemCA: false });
2✔
445
      } catch {}
446
    }
17!
447
    throw error;
17✔
448
  }
17✔
449
}
450

451
async function connectMongoClientImpl({
×
452
  uri,
16✔
453
  clientOptions,
16✔
454
  logger,
10✔
455
  MongoClientClass,
10✔
456
  useSystemCA,
10✔
457
}: {
10✔
458
  uri: string;
10✔
459
  clientOptions: DevtoolsConnectOptions;
10✔
460
  logger: ConnectLogEmitter;
9✔
461
  MongoClientClass: typeof MongoClient;
462
  useSystemCA: boolean;
10✔
463
}): Promise<ConnectMongoClientResult> {
464
  const cleanupOnClientClose: (() => void | Promise<void>)[] = [];
16✔
465
  const runClose = async () => {
16✔
466
    let item: (() => void | Promise<void>) | undefined;
16✔
467
    while ((item = cleanupOnClientClose.shift())) await item();
×
468
  };
469

470
  let ca: string | undefined;
471
  try {
×
472
    if (useSystemCA) {
×
473
      const {
474
        ca: caWithSystemCerts,
×
475
        asyncFallbackError,
476
        systemCertsError,
477
        systemCACount,
478
        messages,
479
      } = await systemCA({
×
480
        ca: clientOptions.ca,
481
        tlsCAFile:
×
482
          clientOptions.tlsCAFile || getConnectionStringParam(uri, 'tlsCAFile'),
×
483
      });
×
484
      logger.emit('devtools-connect:used-system-ca', {
×
485
        caCount: systemCACount,
486
        asyncFallbackError,
×
487
        systemCertsError,
×
488
        messages,
×
489
      });
490
      ca = caWithSystemCerts;
×
491
    }
492

493
    // Create a proxy agent, if requested. `useOrCreateAgent()` takes a target argument
494
    // that can be used to select a proxy for a specific procotol or host;
495
    // here we specify 'mongodb://' if we only intend to use the proxy for database
496
    // connectivity.
497
    const proxyAgent =
×
498
      clientOptions.proxy &&
×
499
      useOrCreateAgent(
500
        'createConnection' in clientOptions.proxy
×
501
          ? clientOptions.proxy
502
          : {
503
              ...(clientOptions.proxy as DevtoolsProxyOptions),
504
              ...(ca ? { ca } : {}),
×
505
            },
×
506
        clientOptions.applyProxyToOIDC === true ? undefined : 'mongodb://',
×
507
      );
508
    cleanupOnClientClose.push(() => proxyAgent?.destroy());
×
509

510
    let oidcProxyOptions:
×
511
      | AgentWithInitialize
512
      | DevtoolsProxyOptions
513
      | undefined;
514
    if (clientOptions.applyProxyToOIDC === true) {
×
515
      oidcProxyOptions = proxyAgent;
×
516
    } else if (clientOptions.applyProxyToOIDC) {
×
517
      oidcProxyOptions = clientOptions.applyProxyToOIDC;
×
518
    }
×
519
    if (!oidcProxyOptions && ca) {
×
520
      oidcProxyOptions = { ca };
×
521
    }
×
522
    if (oidcProxyOptions) {
×
523
      clientOptions.oidc = {
×
524
        customFetch: createFetch(
×
525
          oidcProxyOptions,
526
        ) as unknown as MongoDBOIDCPluginOptions['customFetch'],
527
        ...clientOptions.oidc,
×
528
      };
529
    }
×
530

531
    let tunnel: Tunnel | undefined;
×
532
    if (proxyAgent && !hasProxyHostOption(uri, clientOptions)) {
×
533
      tunnel = createSocks5Tunnel(
×
534
        proxyAgent,
535
        'generate-credentials',
536
        'mongodb://',
537
      );
538
      cleanupOnClientClose.push(() => tunnel?.close());
×
539
    }
540
    for (const proxyLogger of new Set([tunnel?.logger, proxyAgent?.logger])) {
×
541
      if (proxyLogger) {
×
542
        copyEventEmitterEvents(proxyLogger, logger);
×
543
      }
544
    }
×
545
    if (tunnel) {
×
546
      // Should happen after attaching loggers
547
      await tunnel?.listen();
×
548
      clientOptions = {
×
549
        ...clientOptions,
×
550
        ...tunnel?.config,
×
551
      };
×
552
    }
553

554
    // If PROVIDER_NAME was specified to the MongoClient options, adding callbacks would conflict
555
    // with that; we should omit them so that e.g. mongosh users can leverage the non-human OIDC
556
    // auth flows by specifying PROVIDER_NAME.
557
    const shouldAddOidcCallbacks = isHumanOidcFlow(uri, clientOptions);
×
558
    const state =
×
559
      clientOptions.parentState ??
×
560
      new DevtoolsConnectionState(clientOptions, logger);
×
561
    const mongoClientOptions: MongoClientOptions &
×
562
      Partial<DevtoolsConnectOptions> = merge(
×
563
      { __skipPingOnConnect: true },
564
      clientOptions,
565
      shouldAddOidcCallbacks ? state.oidcPlugin.mongoClientOptions : {},
×
566
      { allowPartialTrustChain: true },
567
      ca ? { ca } : {},
×
568
    );
569

570
    // Adopt dns result order changes with Node v18 that affected the VSCode extension VSCODE-458.
571
    // Refs https://github.com/microsoft/vscode/issues/189805
572
    mongoClientOptions.lookup = (hostname, options, callback) => {
×
573
      return dns.lookup(hostname, { verbatim: false, ...options }, callback);
×
574
    };
575

576
    delete (mongoClientOptions as any).useSystemCA; // can be removed once no product uses this anymore
×
577
    delete mongoClientOptions.productDocsLink;
×
578
    delete mongoClientOptions.productName;
×
579
    delete mongoClientOptions.oidc;
×
580
    delete mongoClientOptions.parentState;
×
581
    delete mongoClientOptions.parentHandle;
×
582
    delete mongoClientOptions.proxy;
×
583
    delete mongoClientOptions.applyProxyToOIDC;
×
584

585
    if (
×
586
      mongoClientOptions.autoEncryption !== undefined &&
×
587
      !mongoClientOptions.autoEncryption.bypassAutoEncryption &&
×
588
      !mongoClientOptions.autoEncryption.bypassQueryAnalysis
×
589
    ) {
590
      // connect first without autoEncryption and serverApi options.
591
      const optionsWithoutFLE = { ...mongoClientOptions };
×
592
      delete optionsWithoutFLE.autoEncryption;
×
593
      delete optionsWithoutFLE.serverApi;
×
594
      const client = new MongoClientClass(uri, optionsWithoutFLE);
×
595
      closeMongoClientWhenAuthFails(state, client);
×
596
      await connectWithFailFast(uri, client, logger);
×
597
      const buildInfo = await client
×
598
        .db('admin')
599
        .admin()
×
600
        .command({ buildInfo: 1 });
×
601
      await client.close();
×
602
      if (
×
603
        !buildInfo.modules?.includes('enterprise') &&
×
604
        !buildInfo.gitVersion?.match(/enterprise/)
×
605
      ) {
606
        throw new MongoAutoencryptionUnavailable();
×
607
      }
×
608
    }
609
    uri = await resolveMongodbSrv(uri, logger);
×
610
    const client = new MongoClientClass(uri, mongoClientOptions);
×
611
    client.once('close', runClose);
×
612
    closeMongoClientWhenAuthFails(state, client);
×
613
    await connectWithFailFast(uri, client, logger);
×
614
    if ((client as any).autoEncrypter) {
×
615
      // Enable Devtools-specific CSFLE result decoration.
616
      (client as any).autoEncrypter[
×
617
        Symbol.for('@@mdb.decorateDecryptionResult')
618
      ] = true;
619
    }
620
    return { client, state };
×
621
  } catch (err: unknown) {
×
622
    await runClose();
×
623
    throw err;
×
624
  }
×
625
}
×
626

627
function getMaybeConectionString(uri: string): ConnectionString | null {
628
  try {
×
629
    return new ConnectionString(uri, { looseValidation: true });
×
630
  } catch {
×
631
    return null;
×
632
  }
633
}
634

635
function getConnectionStringParam<K extends keyof MongoClientOptions>(
636
  uri: string,
637
  key: K,
638
): string | null {
639
  return (
×
640
    getMaybeConectionString(uri)
×
641
      ?.typedSearchParams<MongoClientOptions>()
×
642
      .get(key) ?? null
643
  );
644
}
645

646
function hasProxyHostOption(
×
647
  uri: string,
648
  clientOptions: MongoClientOptions,
649
): boolean {
650
  if (clientOptions.proxyHost || clientOptions.proxyPort) return true;
×
651
  const sp =
652
    getMaybeConectionString(uri)?.typedSearchParams<MongoClientOptions>();
×
653
  return sp?.has('proxyHost') || sp?.has('proxyPort') || false;
×
654
}
655

656
export function isHumanOidcFlow(
1✔
657
  uri: string,
658
  clientOptions: MongoClientOptions,
659
): boolean {
660
  if (
×
661
    (clientOptions.authMechanism &&
×
662
      clientOptions.authMechanism !== 'MONGODB-OIDC') ||
663
    clientOptions.authMechanismProperties?.ENVIRONMENT ||
×
664
    clientOptions.authMechanismProperties?.OIDC_CALLBACK
×
665
  ) {
666
    return false;
×
667
  }
×
668

669
  const sp =
670
    getMaybeConectionString(uri)?.typedSearchParams<MongoClientOptions>();
×
671
  const authMechanism = clientOptions.authMechanism ?? sp?.get('authMechanism');
×
672
  return (
×
673
    authMechanism === 'MONGODB-OIDC' &&
×
674
    !new CommaAndColonSeparatedRecord(sp?.get('authMechanismProperties')).get(
675
      'ENVIRONMENT',
676
    )
677
  );
678
}
679

680
function closeMongoClientWhenAuthFails(
681
  state: DevtoolsConnectionState,
682
  client: MongoClient,
683
): void {
684
  // First, make sure that the 'close' event is emitted on the client,
685
  // see also the comments in https://jira.mongodb.org/browse/NODE-5155.
686
  // eslint-disable-next-line @typescript-eslint/unbound-method
687
  const originalClose = client.close;
×
688
  client.close = async function (...args) {
×
689
    let closeEmitted = false;
×
690
    const onClose = () => (closeEmitted = true);
×
691
    this.on('close', onClose);
×
692
    const result = await originalClose.call(this, ...args);
×
693
    this.off('close', onClose);
×
694
    if (!closeEmitted) {
×
695
      this.emit('close');
×
696
    }
697
    return result;
×
698
  };
×
699

700
  // Close the client when the OIDC plugin says that authentication failed
701
  // (notably, this also happens when that failure comes from another
702
  // client using the same `state` instance).
703
  // eslint-disable-next-line @typescript-eslint/no-empty-function
704
  const onOIDCAuthFailed = () => client.close().catch(() => {});
×
705
  state.oidcPlugin.logger.once(
×
706
    'mongodb-oidc-plugin:auth-failed',
707
    onOIDCAuthFailed,
708
  );
709
  client.once('close', () =>
×
710
    state.oidcPlugin.logger.off?.(
×
711
      'mongodb-oidc-plugin:auth-failed',
712
      onOIDCAuthFailed,
713
    ),
714
  );
715
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc