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

mongodb-js / devtools-shared / 15214103202

23 May 2025 03:41PM CUT coverage: 72.383%. Remained the same
15214103202

Pull #541

github

lerouxb
make shell-api a peer dep
Pull Request #541: chore: make shell-api a peer dep

1478 of 2319 branches covered (63.73%)

Branch coverage included in aggregate %.

3203 of 4148 relevant lines covered (77.22%)

592.9 hits per line

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

33.12
/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
  HttpOptions as OIDCHTTPOptions,
×
17
  MongoDBOIDCPlugin,
18
  MongoDBOIDCPluginOptions,
1✔
19
} from '@mongodb-js/oidc-plugin';
1✔
20
import { createMongoDBOIDCPlugin } from '@mongodb-js/oidc-plugin';
2!
21
import merge from 'lodash.merge';
1✔
22
import { oidcServerRequestHandler } from './oidc/handler';
1!
23
import { StateShareClient, StateShareServer } from './ipc-rpc-state-share';
1✔
24
import ConnectionString, {
2✔
25
  CommaAndColonSeparatedRecord,
×
26
} from 'mongodb-connection-string-url';
27
import { EventEmitter } from 'events';
1✔
28
import {
2!
29
  createSocks5Tunnel,
×
30
  DevtoolsProxyOptions,
×
31
  AgentWithInitialize,
×
32
  useOrCreateAgent,
×
33
  Tunnel,
34
  systemCA,
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>(
289
      event: K,
1✔
290
      ...args: M[K] extends (...args: infer P) => any ? P : never
291
    ) => void;
13✔
292
  },
13✔
293
  to: {
13✔
294
    emit: <K extends string & keyof M>(
13✔
295
      event: K,
296
      ...args: M[K] extends (...args: infer P) => any ? P : never
297
    ) => void;
7✔
298
  },
1✔
299
) {
300
  from.emit = function <K extends string & keyof M>(
×
301
    event: K,
1✔
302
    ...args: M[K] extends (...args: infer P) => any ? P : never
1✔
303
  ) {
304
    to.emit(event, ...args);
×
305
    return EventEmitter.prototype.emit.call(this, event, ...args);
×
306
  };
7✔
307
}
308

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

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

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

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

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

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

8✔
411
export type ConnectMongoClientResult = {
412
  client: MongoClient;
413
  state: DevtoolsConnectionState;
414
};
30✔
415

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

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

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

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

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

511
    if (clientOptions.applyProxyToOIDC) {
×
512
      clientOptions.oidc = {
×
513
        ...clientOptions.oidc,
514
        ...addToOIDCPluginHttpOptions(clientOptions.oidc, {
515
          agent: proxyAgent,
516
        }),
×
517
      };
×
518
    }
×
519

520
    let tunnel: Tunnel | undefined;
×
521
    if (proxyAgent && !hasProxyHostOption(uri, clientOptions)) {
×
522
      tunnel = createSocks5Tunnel(
×
523
        proxyAgent,
524
        'generate-credentials',
525
        'mongodb://',
526
      );
527
      cleanupOnClientClose.push(() => tunnel?.close());
×
528
    }
529
    for (const proxyLogger of new Set([tunnel?.logger, proxyAgent?.logger])) {
×
530
      if (proxyLogger) {
×
531
        copyEventEmitterEvents(proxyLogger, logger);
×
532
      }
533
    }
×
534
    if (tunnel) {
×
535
      // Should happen after attaching loggers
536
      await tunnel?.listen();
×
537
      clientOptions = {
×
538
        ...clientOptions,
×
539
        ...tunnel?.config,
×
540
      };
×
541
    }
542

543
    // If PROVIDER_NAME was specified to the MongoClient options, adding callbacks would conflict
544
    // with that; we should omit them so that e.g. mongosh users can leverage the non-human OIDC
545
    // auth flows by specifying PROVIDER_NAME.
546
    const shouldAddOidcCallbacks = isHumanOidcFlow(uri, clientOptions);
×
547
    const state =
×
548
      clientOptions.parentState ??
×
549
      new DevtoolsConnectionState(clientOptions, logger, ca);
×
550
    const mongoClientOptions: MongoClientOptions &
×
551
      Partial<DevtoolsConnectOptions> = merge(
×
552
      {},
553
      clientOptions,
554
      shouldAddOidcCallbacks ? state.oidcPlugin.mongoClientOptions : {},
×
555
      { allowPartialTrustChain: true },
556
      ca ? { ca } : {},
×
557
    );
558

559
    // Adopt dns result order changes with Node v18 that affected the VSCode extension VSCODE-458.
560
    // Refs https://github.com/microsoft/vscode/issues/189805
561
    mongoClientOptions.lookup = (hostname, options, callback) => {
×
562
      return dns.lookup(hostname, { verbatim: false, ...options }, callback);
×
563
    };
564

565
    delete (mongoClientOptions as any).useSystemCA; // can be removed once no product uses this anymore
×
566
    delete mongoClientOptions.productDocsLink;
×
567
    delete mongoClientOptions.productName;
×
568
    delete mongoClientOptions.oidc;
×
569
    delete mongoClientOptions.parentState;
×
570
    delete mongoClientOptions.parentHandle;
×
571
    delete mongoClientOptions.proxy;
×
572
    delete mongoClientOptions.applyProxyToOIDC;
×
573

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

616
function getMaybeConectionString(uri: string): ConnectionString | null {
617
  try {
×
618
    return new ConnectionString(uri, { looseValidation: true });
×
619
  } catch {
×
620
    return null;
×
621
  }
622
}
623

624
function getConnectionStringParam<K extends keyof MongoClientOptions>(
625
  uri: string,
626
  key: K,
627
): string | null {
628
  return (
×
629
    getMaybeConectionString(uri)
×
630
      ?.typedSearchParams<MongoClientOptions>()
×
631
      .get(key) ?? null
632
  );
633
}
634

635
function hasProxyHostOption(
×
636
  uri: string,
637
  clientOptions: MongoClientOptions,
638
): boolean {
639
  if (clientOptions.proxyHost || clientOptions.proxyPort) return true;
×
640
  const sp =
641
    getMaybeConectionString(uri)?.typedSearchParams<MongoClientOptions>();
×
642
  return sp?.has('proxyHost') || sp?.has('proxyPort') || false;
×
643
}
644

645
export function isHumanOidcFlow(
1✔
646
  uri: string,
647
  clientOptions: MongoClientOptions,
648
): boolean {
649
  if (
×
650
    (clientOptions.authMechanism &&
×
651
      clientOptions.authMechanism !== 'MONGODB-OIDC') ||
652
    clientOptions.authMechanismProperties?.ENVIRONMENT ||
×
653
    clientOptions.authMechanismProperties?.OIDC_CALLBACK
×
654
  ) {
655
    return false;
×
656
  }
×
657

658
  const sp =
659
    getMaybeConectionString(uri)?.typedSearchParams<MongoClientOptions>();
×
660
  const authMechanism = clientOptions.authMechanism ?? sp?.get('authMechanism');
×
661
  return (
×
662
    authMechanism === 'MONGODB-OIDC' &&
×
663
    !new CommaAndColonSeparatedRecord(sp?.get('authMechanismProperties')).get(
664
      'ENVIRONMENT',
665
    )
666
  );
667
}
668

669
function closeMongoClientWhenAuthFails(
670
  state: DevtoolsConnectionState,
671
  client: MongoClient,
672
): void {
673
  // First, make sure that the 'close' event is emitted on the client,
674
  // see also the comments in https://jira.mongodb.org/browse/NODE-5155.
675
  // eslint-disable-next-line @typescript-eslint/unbound-method
676
  const originalClose = client.close;
×
677
  client.close = async function (...args) {
×
678
    let closeEmitted = false;
×
679
    const onClose = () => (closeEmitted = true);
×
680
    this.on('close', onClose);
×
681
    const result = await originalClose.call(this, ...args);
×
682
    this.off('close', onClose);
×
683
    if (!closeEmitted) {
×
684
      this.emit('close');
×
685
    }
×
686
    return result;
×
687
  };
×
688

689
  // Close the client when the OIDC plugin says that authentication failed
690
  // (notably, this also happens when that failure comes from another
691
  // client using the same `state` instance).
692
  // eslint-disable-next-line @typescript-eslint/no-empty-function
693
  const onOIDCAuthFailed = () => client.close().catch(() => {});
×
694
  state.oidcPlugin.logger.once(
×
695
    'mongodb-oidc-plugin:auth-failed',
696
    onOIDCAuthFailed,
697
  );
698
  client.once('close', () =>
×
699
    state.oidcPlugin.logger.off?.(
×
700
      'mongodb-oidc-plugin:auth-failed',
701
      onOIDCAuthFailed,
702
    ),
703
  );
704
}
×
705

706
function addToOIDCPluginHttpOptions(
×
707
  existingOIDCPluginOptions: MongoDBOIDCPluginOptions | undefined,
708
  addedOptions: Partial<OIDCHTTPOptions>,
709
): Pick<MongoDBOIDCPluginOptions, 'customHttpOptions'> {
710
  const existingCustomOptions = existingOIDCPluginOptions?.customHttpOptions;
×
711
  if (typeof existingCustomOptions === 'function') {
×
712
    return {
×
713
      customHttpOptions: (url, options, ...restArgs) =>
×
714
        existingCustomOptions(
×
715
          url,
716
          { ...options, ...addedOptions },
717
          ...restArgs,
718
        ),
719
    };
×
720
  }
×
721
  return { customHttpOptions: { ...existingCustomOptions, ...addedOptions } };
×
722
}
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