mongodb 7.7.0 → 7.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/README.md +1 -1
  2. package/lib/bulk/unordered.js +1 -1
  3. package/lib/bulk/unordered.js.map +1 -1
  4. package/lib/change_stream.js +3 -11
  5. package/lib/change_stream.js.map +1 -1
  6. package/lib/client-side-encryption/client_encryption.js.map +1 -1
  7. package/lib/cmap/wire_protocol/compression.js +2 -0
  8. package/lib/cmap/wire_protocol/compression.js.map +1 -1
  9. package/lib/collection.js +18 -119
  10. package/lib/collection.js.map +1 -1
  11. package/lib/connection_string.js +110 -15
  12. package/lib/connection_string.js.map +1 -1
  13. package/lib/cursor/abstract_cursor.js +9 -62
  14. package/lib/cursor/abstract_cursor.js.map +1 -1
  15. package/lib/cursor/aggregation_cursor.js +0 -9
  16. package/lib/cursor/aggregation_cursor.js.map +1 -1
  17. package/lib/cursor/find_cursor.js +0 -9
  18. package/lib/cursor/find_cursor.js.map +1 -1
  19. package/lib/cursor/list_collections_cursor.js +1 -1
  20. package/lib/cursor/list_collections_cursor.js.map +1 -1
  21. package/lib/cursor/list_indexes_cursor.js +1 -1
  22. package/lib/cursor/list_indexes_cursor.js.map +1 -1
  23. package/lib/cursor/run_command_cursor.js.map +1 -1
  24. package/lib/db.js +5 -58
  25. package/lib/db.js.map +1 -1
  26. package/lib/deps.js +35 -1
  27. package/lib/deps.js.map +1 -1
  28. package/lib/explain.js +0 -8
  29. package/lib/explain.js.map +1 -1
  30. package/lib/gridfs/download.js +3 -11
  31. package/lib/gridfs/download.js.map +1 -1
  32. package/lib/gridfs/index.js +4 -4
  33. package/lib/gridfs/index.js.map +1 -1
  34. package/lib/gridfs/upload.js +3 -18
  35. package/lib/gridfs/upload.js.map +1 -1
  36. package/lib/mongo_client.js +2 -60
  37. package/lib/mongo_client.js.map +1 -1
  38. package/lib/operations/command.js +0 -2
  39. package/lib/operations/command.js.map +1 -1
  40. package/lib/operations/create_collection.js +1 -1
  41. package/lib/operations/create_collection.js.map +1 -1
  42. package/lib/operations/indexes.js +45 -16
  43. package/lib/operations/indexes.js.map +1 -1
  44. package/lib/sdam/srv_polling.js +7 -2
  45. package/lib/sdam/srv_polling.js.map +1 -1
  46. package/lib/sdam/topology.js +3 -1
  47. package/lib/sdam/topology.js.map +1 -1
  48. package/lib/sessions.js +10 -47
  49. package/lib/sessions.js.map +1 -1
  50. package/lib/sort.js +1 -1
  51. package/lib/sort.js.map +1 -1
  52. package/lib/timeout.js +2 -11
  53. package/lib/timeout.js.map +1 -1
  54. package/lib/utils.js +84 -23
  55. package/lib/utils.js.map +1 -1
  56. package/mongodb.d.ts +249 -314
  57. package/package.json +1 -1
  58. package/src/bulk/unordered.ts +1 -1
  59. package/src/change_stream.ts +4 -12
  60. package/src/client-side-encryption/client_encryption.ts +5 -32
  61. package/src/client-side-encryption/kms_options.ts +1 -0
  62. package/src/cmap/wire_protocol/compression.ts +3 -1
  63. package/src/collection.ts +83 -71
  64. package/src/connection_string.ts +135 -19
  65. package/src/cursor/abstract_cursor.ts +20 -97
  66. package/src/cursor/aggregation_cursor.ts +1 -16
  67. package/src/cursor/find_cursor.ts +2 -17
  68. package/src/cursor/list_collections_cursor.ts +1 -1
  69. package/src/cursor/list_indexes_cursor.ts +1 -1
  70. package/src/cursor/run_command_cursor.ts +4 -31
  71. package/src/db.ts +13 -59
  72. package/src/deps.ts +38 -1
  73. package/src/explain.ts +0 -8
  74. package/src/gridfs/download.ts +7 -14
  75. package/src/gridfs/index.ts +27 -8
  76. package/src/gridfs/upload.ts +6 -21
  77. package/src/index.ts +2 -0
  78. package/src/mongo_client.ts +46 -61
  79. package/src/operations/command.ts +1 -7
  80. package/src/operations/create_collection.ts +4 -2
  81. package/src/operations/drop.ts +1 -1
  82. package/src/operations/indexes.ts +213 -17
  83. package/src/operations/operation.ts +1 -1
  84. package/src/operations/run_command.ts +1 -1
  85. package/src/sdam/srv_polling.ts +12 -3
  86. package/src/sdam/topology.ts +5 -1
  87. package/src/sessions.ts +21 -63
  88. package/src/sort.ts +1 -1
  89. package/src/timeout.ts +2 -13
  90. package/src/utils.ts +96 -30
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mongodb",
3
- "version": "7.7.0",
3
+ "version": "7.8.0",
4
4
  "description": "The official MongoDB driver for Node.js",
5
5
  "main": "lib/index.js",
6
6
  "files": [
@@ -112,7 +112,7 @@ export class UnorderedBulkOperation extends BulkOperationBase {
112
112
  if (batchType === BatchType.INSERT) {
113
113
  this.s.currentInsertBatch = this.s.currentBatch;
114
114
  this.s.bulkResult.insertedIds.push({
115
- index: this.s.bulkResult.insertedIds.length,
115
+ index: this.s.currentIndex - 1,
116
116
  _id: (document as Document)._id
117
117
  });
118
118
  } else if (batchType === BatchType.UPDATE) {
@@ -20,7 +20,7 @@ import type { AggregateOptions } from './operations/aggregate';
20
20
  import type { OperationParent } from './operations/command';
21
21
  import { DeprioritizedServers } from './sdam/server_selection';
22
22
  import type { ServerSessionId } from './sessions';
23
- import { CSOTTimeoutContext, type TimeoutContext } from './timeout';
23
+ import { type TimeoutContext } from './timeout';
24
24
  import { type AnyOptions, getTopology, type MongoDBNamespace, squashError } from './utils';
25
25
 
26
26
  const CHANGE_DOMAIN_TYPES = {
@@ -647,18 +647,17 @@ export class ChangeStream<
647
647
 
648
648
  this.pipeline = pipeline;
649
649
  this.options = { ...options };
650
- let serverSelectionTimeoutMS: number;
650
+ // timeoutMS and timeoutMode are no-ops, so they must never reach the cursor options
651
+ this.options.timeoutMS = undefined;
652
+ this.options.timeoutMode = undefined;
651
653
  delete this.options.writeConcern;
652
654
 
653
655
  if (parent instanceof Collection) {
654
656
  this.type = CHANGE_DOMAIN_TYPES.COLLECTION;
655
- serverSelectionTimeoutMS = parent.s.db.client.options.serverSelectionTimeoutMS;
656
657
  } else if (parent instanceof Db) {
657
658
  this.type = CHANGE_DOMAIN_TYPES.DATABASE;
658
- serverSelectionTimeoutMS = parent.client.options.serverSelectionTimeoutMS;
659
659
  } else if (parent instanceof MongoClient) {
660
660
  this.type = CHANGE_DOMAIN_TYPES.CLUSTER;
661
- serverSelectionTimeoutMS = parent.options.serverSelectionTimeoutMS;
662
661
  } else {
663
662
  throw new MongoChangeStreamError(
664
663
  'Parent provided to ChangeStream constructor must be an instance of Collection, Db, or MongoClient'
@@ -690,13 +689,6 @@ export class ChangeStream<
690
689
  this.cursorStream?.removeAllListeners('data');
691
690
  }
692
691
  });
693
-
694
- if (this.options.timeoutMS != null) {
695
- this.timeoutContext = new CSOTTimeoutContext({
696
- timeoutMS: this.options.timeoutMS,
697
- serverSelectionTimeoutMS
698
- });
699
- }
700
692
  }
701
693
 
702
694
  /** The cached resume token that is used to resume after the most recently returned change. */
@@ -294,7 +294,7 @@ export class ClientEncryption {
294
294
  async rewrapManyDataKey(
295
295
  filter: Filter<DataKey>,
296
296
  options?: ClientEncryptionRewrapManyDataKeyProviderOptions
297
- ): Promise<{ bulkWriteResult?: BulkWriteResult }> {
297
+ ): Promise<ClientEncryptionRewrapManyDataKeyResult> {
298
298
  let keyEncryptionKeyBson = undefined;
299
299
  if (options) {
300
300
  const keyEncryptionKey = Object.assign({ provider: options.provider }, options.masterKey);
@@ -927,7 +927,7 @@ export type TextQueryOptions = StringQueryOptions;
927
927
 
928
928
  /**
929
929
  * @public
930
- * @experimental
930
+ * Options interface for `rewrapManyDataKey`; selects the new KMS provider/masterKey for bulk key rotation.
931
931
  */
932
932
  export interface ClientEncryptionRewrapManyDataKeyProviderOptions {
933
933
  provider: ClientEncryptionDataKeyProvider;
@@ -987,35 +987,8 @@ export interface ClientEncryptionOptions {
987
987
  keyExpirationMS?: number;
988
988
 
989
989
  /**
990
- * @experimental
991
- *
992
990
  * The timeout setting to be used for all the operations on ClientEncryption.
993
- *
994
- * When provided, `timeoutMS` is used as the timeout for each operation executed on
995
- * the ClientEncryption object. For example:
996
- *
997
- * ```typescript
998
- * const clientEncryption = new ClientEncryption(client, {
999
- * timeoutMS: 1_000
1000
- * kmsProviders: { local: { key: '<KEY>' } }
1001
- * });
1002
- *
1003
- * // `1_000` is used as the timeout for createDataKey call
1004
- * await clientEncryption.createDataKey('local');
1005
- * ```
1006
- *
1007
- * If `timeoutMS` is configured on the provided client, the client's `timeoutMS` value
1008
- * will be used unless `timeoutMS` is also provided as a client encryption option.
1009
- *
1010
- * ```typescript
1011
- * const client = new MongoClient('<uri>', { timeoutMS: 2_000 });
1012
- *
1013
- * // timeoutMS is set to 1_000 on clientEncryption
1014
- * const clientEncryption = new ClientEncryption(client, {
1015
- * timeoutMS: 1_000
1016
- * kmsProviders: { local: { key: '<KEY>' } }
1017
- * });
1018
- * ```
991
+ * @deprecated `timeoutMS` has no effect and will be removed in a future release.
1019
992
  */
1020
993
  timeoutMS?: number;
1021
994
  }
@@ -1144,7 +1117,7 @@ export interface ClientEncryptionCreateDataKeyProviderOptions {
1144
1117
  */
1145
1118
  keyAltNames?: string[] | undefined;
1146
1119
 
1147
- /** @experimental */
1120
+ /** Optional 96-byte custom key material used when creating a data key instead of libmongocrypt’s random material. **/
1148
1121
  keyMaterial?: Buffer | Binary;
1149
1122
 
1150
1123
  /** @internal */
@@ -1153,7 +1126,7 @@ export interface ClientEncryptionCreateDataKeyProviderOptions {
1153
1126
 
1154
1127
  /**
1155
1128
  * @public
1156
- * @experimental
1129
+ * Public result shape describing the `bulkWriteResult` produced by `rewrapManyDataKey`.
1157
1130
  */
1158
1131
  export interface ClientEncryptionRewrapManyDataKeyResult {
1159
1132
  /** The result of rewrapping data keys. If unset, no keys matched the filter. */
@@ -88,6 +88,7 @@ export type ClientEncryptionSocketOptions = Pick<
88
88
  export type KMSConnectCallback = (options: {
89
89
  host: string;
90
90
  port: number;
91
+ /** @deprecated `timeoutMS` has no effect and will be removed in a future release. */
91
92
  timeoutMS?: number;
92
93
  signal: AbortSignal;
93
94
  }) => Promise<Duplex>;
@@ -1,7 +1,7 @@
1
1
  import * as zlib from 'zlib';
2
2
 
3
3
  import { ByteUtils, readInt32LE } from '../../bson';
4
- import { LEGACY_HELLO_COMMAND } from '../../constants';
4
+ import { LEGACY_HELLO_COMMAND, LEGACY_HELLO_COMMAND_CAMEL_CASE } from '../../constants';
5
5
  import { getSnappy, getZstdLibrary, type SnappyLib, type ZStandard } from '../../deps';
6
6
  import { MongoDecompressionError, MongoInvalidArgumentError } from '../../error';
7
7
  import {
@@ -29,7 +29,9 @@ export type Compressor = (typeof Compressor)[CompressorName];
29
29
  export type CompressorName = keyof typeof Compressor;
30
30
 
31
31
  export const uncompressibleCommands = new Set([
32
+ 'hello',
32
33
  LEGACY_HELLO_COMMAND,
34
+ LEGACY_HELLO_COMMAND_CAMEL_CASE,
33
35
  'saslStart',
34
36
  'saslContinue',
35
37
  'getnonce',
package/src/collection.ts CHANGED
@@ -56,12 +56,14 @@ import {
56
56
  import {
57
57
  CreateIndexesOperation,
58
58
  type CreateIndexesOptions,
59
+ type CreateIndexOptions,
59
60
  type DropIndexesOptions,
60
61
  DropIndexOperation,
61
62
  type IndexDescription,
62
63
  type IndexDescriptionCompact,
63
64
  type IndexDescriptionInfo,
64
65
  type IndexInformationOptions,
66
+ type IndexOptions,
65
67
  type IndexSpecification,
66
68
  type ListIndexesOptions
67
69
  } from './operations/indexes';
@@ -120,8 +122,8 @@ export interface CollectionOptions extends BSONSerializeOptions, WriteConcernOpt
120
122
  /** The preferred read preference (ReadPreference.PRIMARY, ReadPreference.PRIMARY_PREFERRED, ReadPreference.SECONDARY, ReadPreference.SECONDARY_PREFERRED, ReadPreference.NEAREST). */
121
123
  readPreference?: ReadPreferenceLike;
122
124
  /**
123
- * @experimental
124
125
  * Specifies the time an operation will run until it throws a timeout error
126
+ * @deprecated `timeoutMS` has no effect and will be removed in a future release.
125
127
  */
126
128
  timeoutMS?: number;
127
129
  }
@@ -275,8 +277,9 @@ export class Collection<TSchema extends Document = Document> {
275
277
  this.s.collectionHint = normalizeHintField(v);
276
278
  }
277
279
 
280
+ /** @deprecated `timeoutMS` has no effect and will be removed in a future release. */
278
281
  public get timeoutMS(): number | undefined {
279
- return this.s.options.timeoutMS;
282
+ return undefined;
280
283
  }
281
284
 
282
285
  /**
@@ -609,7 +612,7 @@ export class Collection<TSchema extends Document = Document> {
609
612
  * Creates an index on the db and collection collection.
610
613
  *
611
614
  * @param indexSpec - The field name or index specification to create an index for
612
- * @param options - Optional settings for the command
615
+ * @param indexOptions - Optional settings for the command
613
616
  *
614
617
  * @example
615
618
  * ```ts
@@ -632,20 +635,55 @@ export class Collection<TSchema extends Document = Document> {
632
635
  * // Equivalent to { j: 1, k: -1, l: 2d }
633
636
  * await collection.createIndex(['j', ['k', -1], { l: '2d' }])
634
637
  * ```
638
+ * @deprecated Use the three parameter overload, which separates index options from
639
+ * command options. This overload will be removed in a future major release.
640
+ */
641
+ createIndex(indexSpec: IndexSpecification, options?: CreateIndexesOptions): Promise<string>;
642
+
643
+ /**
644
+ * Creates an index on the db and collection collection.
645
+ *
646
+ * Options for the index itself are given in `indexOptions`, and options for the
647
+ * `createIndexes` command are given separately in `commandOptions`. Unknown fields in
648
+ * `indexOptions` are passed through to the server for validation rather than being dropped.
649
+ *
650
+ * @param keys - The field name or index specification to create an index for
651
+ * @param indexOptions - Settings for the index, or `undefined` if there are none
652
+ * @param commandOptions - Settings for the `createIndexes` command, or `undefined` if there are none
635
653
  */
654
+ createIndex(
655
+ keys: IndexSpecification,
656
+ indexOptions: IndexOptions | undefined,
657
+ commandOptions: CreateIndexOptions | undefined
658
+ ): Promise<string>;
659
+
636
660
  async createIndex(
637
661
  indexSpec: IndexSpecification,
638
- options?: CreateIndexesOptions
662
+ indexOptions?: CreateIndexesOptions | IndexOptions,
663
+ commandOptions?: CreateIndexOptions
639
664
  ): Promise<string> {
640
- const indexes = await executeOperation(
641
- this.client,
642
- CreateIndexesOperation.fromIndexSpecification(
643
- this,
644
- this.collectionName,
645
- indexSpec,
646
- resolveOptions(this, options)
647
- )
648
- );
665
+ // When commandOptions is provided the caller has separated the two kinds of options, so the
666
+ // index options are forwarded as-is and only the command options inherit from the parent.
667
+ // Otherwise indexOptions is both, and it inherits from the parent as it always has.
668
+ const operation =
669
+ commandOptions == null
670
+ ? CreateIndexesOperation.fromIndexSpecification(
671
+ this,
672
+ this.collectionName,
673
+ indexSpec,
674
+ false,
675
+ resolveOptions(this, indexOptions), // at this point indexOptions is the combined index and command options
676
+ undefined
677
+ )
678
+ : CreateIndexesOperation.fromIndexSpecification(
679
+ this,
680
+ this.collectionName,
681
+ indexSpec,
682
+ true,
683
+ indexOptions,
684
+ resolveOptions(this, commandOptions)
685
+ );
686
+ const indexes = await executeOperation(this.client, operation);
649
687
 
650
688
  return indexes[0];
651
689
  }
@@ -659,7 +697,7 @@ export class Collection<TSchema extends Document = Document> {
659
697
  * Index specifications are defined {@link https://www.mongodb.com/docs/manual/reference/command/createIndexes/| here}.
660
698
  *
661
699
  * @param indexSpecs - An array of index specifications to be created
662
- * @param options - Optional settings for the command
700
+ * @param commandOptions - Optional settings for the `createIndexes` command
663
701
  *
664
702
  * @example
665
703
  * ```ts
@@ -681,9 +719,35 @@ export class Collection<TSchema extends Document = Document> {
681
719
  * ]);
682
720
  * ```
683
721
  */
722
+ createIndexes(
723
+ indexSpecs: IndexDescription[],
724
+ commandOptions?: CreateIndexesOptions
725
+ ): Promise<string[]>;
726
+
727
+ /**
728
+ * Creates multiple indexes in the collection, opting into "pass through" behavior for index
729
+ * options the driver does not recognise.
730
+ *
731
+ * @param indexSpecs - An array of index specifications to be created
732
+ * @param commandOptions - Optional settings for the `createIndexes` command
733
+ * @param allowUnknownIndexOptions - When `true`, index options the driver does not recognise are
734
+ * sent to the server instead of being dropped, for the server to validate.
735
+ *
736
+ * @deprecated Used to opt into "pass through" behavior, where options will be validated by the
737
+ * server rather than the driver. In a future release this overload will be removed and the
738
+ * default behavior will change from `false` to `true`.
739
+ */
740
+ createIndexes(
741
+ indexSpecs: IndexDescription[],
742
+ commandOptions: CreateIndexesOptions | undefined,
743
+ allowUnknownIndexOptions: boolean
744
+ ): Promise<string[]>;
745
+
684
746
  async createIndexes(
685
747
  indexSpecs: IndexDescription[],
686
- options?: CreateIndexesOptions
748
+ commandOptions?: CreateIndexesOptions,
749
+ // TODO(NODE-7868): Remove allowUnknownIndexOptions with a default behavior of true in a future major version release
750
+ allowUnknownIndexOptions = false
687
751
  ): Promise<string[]> {
688
752
  return await executeOperation(
689
753
  this.client,
@@ -691,7 +755,9 @@ export class Collection<TSchema extends Document = Document> {
691
755
  this,
692
756
  this.collectionName,
693
757
  indexSpecs,
694
- resolveOptions(this, { ...options, maxTimeMS: undefined })
758
+ // TODO(NODE-7868): Remove allowUnknownIndexOptions with a default behavior of true in a future major version release
759
+ allowUnknownIndexOptions,
760
+ resolveOptions(this, { ...commandOptions, maxTimeMS: undefined })
695
761
  )
696
762
  );
697
763
  }
@@ -1105,60 +1171,6 @@ export class Collection<TSchema extends Document = Document> {
1105
1171
  * expectType<Schema>(change.fullDocument);
1106
1172
  * });
1107
1173
  * ```
1108
- *
1109
- * @remarks
1110
- * When `timeoutMS` is configured for a change stream, it will have different behaviour depending
1111
- * on whether the change stream is in iterator mode or emitter mode. In both cases, a change
1112
- * stream will time out if it does not receive a change event within `timeoutMS` of the last change
1113
- * event.
1114
- *
1115
- * Note that if a change stream is consistently timing out when watching a collection, database or
1116
- * client that is being changed, then this may be due to the server timing out before it can finish
1117
- * processing the existing oplog. To address this, restart the change stream with a higher
1118
- * `timeoutMS`.
1119
- *
1120
- * If the change stream times out the initial aggregate operation to establish the change stream on
1121
- * the server, then the client will close the change stream. If the getMore calls to the server
1122
- * time out, then the change stream will be left open, but will throw a MongoOperationTimeoutError
1123
- * when in iterator mode and emit an error event that returns a MongoOperationTimeoutError in
1124
- * emitter mode.
1125
- *
1126
- * To determine whether or not the change stream is still open following a timeout, check the
1127
- * {@link ChangeStream.closed} getter.
1128
- *
1129
- * @example
1130
- * In iterator mode, if a next() call throws a timeout error, it will attempt to resume the change stream.
1131
- * The next call can just be retried after this succeeds.
1132
- * ```ts
1133
- * const changeStream = collection.watch([], { timeoutMS: 100 });
1134
- * try {
1135
- * await changeStream.next();
1136
- * } catch (e) {
1137
- * if (e instanceof MongoOperationTimeoutError && !changeStream.closed) {
1138
- * await changeStream.next();
1139
- * }
1140
- * throw e;
1141
- * }
1142
- * ```
1143
- *
1144
- * @example
1145
- * In emitter mode, if the change stream goes `timeoutMS` without emitting a change event, it will
1146
- * emit an error event that returns a MongoOperationTimeoutError, but will not close the change
1147
- * stream unless the resume attempt fails. There is no need to re-establish change listeners as
1148
- * this will automatically continue emitting change events once the resume attempt completes.
1149
- *
1150
- * ```ts
1151
- * const changeStream = collection.watch([], { timeoutMS: 100 });
1152
- * changeStream.on('change', console.log);
1153
- * changeStream.on('error', e => {
1154
- * if (e instanceof MongoOperationTimeoutError && !changeStream.closed) {
1155
- * // do nothing
1156
- * } else {
1157
- * changeStream.close();
1158
- * }
1159
- * });
1160
- * ```
1161
- *
1162
1174
  * @param pipeline - An array of {@link https://www.mongodb.com/docs/manual/reference/operator/aggregation-pipeline/|aggregation pipeline stages} through which to pass change stream documents. This allows for filtering (using $match) and manipulating the change stream documents.
1163
1175
  * @param options - Optional settings for the command
1164
1176
  * @typeParam TLocal - Type of the data being detected by the change stream
@@ -1251,7 +1263,7 @@ export class Collection<TSchema extends Document = Document> {
1251
1263
  ? null
1252
1264
  : indexNameOrOptions;
1253
1265
 
1254
- return new ListSearchIndexesCursor(this as TODO_NODE_3286, indexName, options);
1266
+ return new ListSearchIndexesCursor(this as TODO_NODE_3286, indexName, { ...options });
1255
1267
  }
1256
1268
 
1257
1269
  /**
@@ -24,19 +24,46 @@ import { resolveRuntimeAdapters } from './runtime_adapters';
24
24
  import { ServerMonitoringMode } from './sdam/monitor';
25
25
  import type { TagSet } from './sdam/server_description';
26
26
  import {
27
- checkParentDomainMatch,
28
27
  DEFAULT_PK_FACTORY,
28
+ dnsNameToASCII,
29
29
  emitWarning,
30
30
  HostAddress,
31
31
  isRecord,
32
+ normalizeDnsName,
32
33
  parseInteger,
33
34
  setDifference,
34
- squashError
35
+ squashError,
36
+ verifySrvHost
35
37
  } from './utils';
36
38
  import { type W, WriteConcern } from './write_concern';
37
39
 
38
40
  const VALID_TXT_RECORDS = ['authSource', 'replicaSet', 'loadBalanced'];
39
41
 
42
+ /** Options that only apply to SRV resolution, and so are invalid with a non-SRV connection string */
43
+ const SRV_ONLY_OPTIONS = [
44
+ 'srvMaxHosts',
45
+ 'srvServiceName',
46
+ 'srvAllowedHostsSuffix',
47
+ 'srvHostValidator'
48
+ ] as const;
49
+
50
+ /** Single labels that srvAllowedHostsSuffix may be, as they are reserved for private or special use */
51
+ const VALID_SINGLE_LABEL_SUFFIXES = new Set([
52
+ // RFC 6761 special use names
53
+ 'test',
54
+ 'localhost',
55
+ 'invalid',
56
+ 'example',
57
+ // RFC 6762 multicast DNS
58
+ 'local',
59
+ // Reserved by ICANN for private use
60
+ 'internal',
61
+ // Not officially reserved by ICANN, but commonly used privately
62
+ 'corp',
63
+ 'home',
64
+ 'mail'
65
+ ]);
66
+
40
67
  const LB_SINGLE_HOST_ERROR = 'loadBalanced option only supported with a single host in the URI';
41
68
  const LB_REPLICA_SET_ERROR = 'loadBalanced option not supported with a replicaSet option';
42
69
  const LB_DIRECT_CONNECTION_ERROR =
@@ -73,6 +100,9 @@ const resolveTxt = retryDNSTimeoutFor('TXT');
73
100
  *
74
101
  * @param uri - The connection string to parse
75
102
  * @param options - Optional user provided connection string options
103
+ * @throws MongoAPIError if the SRV lookup returns no hosts, or a returned host fails verification
104
+ * @throws MongoParseError if the TXT record is invalid, or conflicts with the provided options
105
+ * @throws Error the error from the DNS lookup, if it fails
76
106
  */
77
107
  export async function resolveSRVRecord(options: MongoOptions): Promise<HostAddress[]> {
78
108
  if (typeof options.srvHost !== 'string') {
@@ -94,11 +124,11 @@ export async function resolveSRVRecord(options: MongoOptions): Promise<HostAddre
94
124
  throw new MongoAPIError('No addresses found at host');
95
125
  }
96
126
 
97
- for (const { name } of addresses) {
98
- checkParentDomainMatch(name, lookupAddress);
99
- }
100
-
101
- const hostAddresses = addresses.map(r => HostAddress.fromString(`${r.name}:${r.port ?? 27017}`));
127
+ const hostAddresses = addresses.map(({ name, port }) => {
128
+ const host = normalizeDnsName(name);
129
+ verifySrvHost(host, lookupAddress, options);
130
+ return HostAddress.fromString(`${host}:${port ?? 27017}`);
131
+ });
102
132
 
103
133
  validateLoadBalancedOptions(hostAddresses, options, true);
104
134
 
@@ -294,7 +324,7 @@ export function parseOptions(
294
324
  );
295
325
  }
296
326
 
297
- if (!isReadPreferenceTags && values.includes('')) {
327
+ if (!isReadPreferenceTags && values.includes('') && !isIgnoredOption(key)) {
298
328
  throw new MongoAPIError(`URI option "${key}" cannot be specified with no value`);
299
329
  }
300
330
 
@@ -315,6 +345,12 @@ export function parseOptions(
315
345
  );
316
346
  }
317
347
 
348
+ if (urlOptions.has('srvHostValidator')) {
349
+ throw new MongoParseError(
350
+ 'URI cannot contain `srvHostValidator`, it can only be passed to the client'
351
+ );
352
+ }
353
+
318
354
  const uriMechanismProperties = urlOptions.get('authMechanismProperties');
319
355
  if (uriMechanismProperties) {
320
356
  for (const property of uriMechanismProperties) {
@@ -380,12 +416,14 @@ export function parseOptions(
380
416
  setOption(mongoOptions, key, descriptor, [DEFAULT_OPTIONS.get(key)]);
381
417
  }
382
418
  } else {
383
- const { deprecated } = descriptor;
419
+ const { deprecated, ignored } = descriptor;
384
420
  if (deprecated) {
385
421
  const deprecatedMsg = typeof deprecated === 'string' ? `: ${deprecated}` : '';
386
422
  emitWarning(`${key} is a deprecated option${deprecatedMsg}`);
387
423
  }
388
424
 
425
+ if (ignored) continue;
426
+
389
427
  setOption(mongoOptions, key, descriptor, values);
390
428
  }
391
429
  }
@@ -477,6 +515,10 @@ export function parseOptions(
477
515
  throw new MongoParseError('Cannot use srvMaxHosts option with replicaSet');
478
516
  }
479
517
 
518
+ if (mongoOptions.srvAllowedHostsSuffix != null && mongoOptions.srvHostValidator != null) {
519
+ throw new MongoParseError('Cannot use srvAllowedHostsSuffix together with srvHostValidator');
520
+ }
521
+
480
522
  // SRV turns on TLS by default, but users can override and turn it off
481
523
  const noUserSpecifiedTLS = !objectOptions.has('tls') && !urlOptions.has('tls');
482
524
  const noUserSpecifiedSSL = !objectOptions.has('ssl') && !urlOptions.has('ssl');
@@ -484,15 +526,13 @@ export function parseOptions(
484
526
  mongoOptions.tls = true;
485
527
  }
486
528
  } else {
487
- const userSpecifiedSrvOptions =
488
- urlOptions.has('srvMaxHosts') ||
489
- objectOptions.has('srvMaxHosts') ||
490
- urlOptions.has('srvServiceName') ||
491
- objectOptions.has('srvServiceName');
529
+ const userSpecifiedSrvOptions = SRV_ONLY_OPTIONS.filter(
530
+ option => urlOptions.has(option) || objectOptions.has(option)
531
+ );
492
532
 
493
- if (userSpecifiedSrvOptions) {
533
+ if (userSpecifiedSrvOptions.length > 0) {
494
534
  throw new MongoParseError(
495
- 'Cannot use srvMaxHosts or srvServiceName with a non-srv connection string'
535
+ `Cannot use ${userSpecifiedSrvOptions.join(', ')} with a non-srv connection string`
496
536
  );
497
537
  }
498
538
  }
@@ -584,6 +624,50 @@ function validateLoadBalancedOptions(
584
624
  return;
585
625
  }
586
626
 
627
+ /**
628
+ * Validates and normalizes an `srvAllowedHostsSuffix` value, following the steps in the Initial DNS
629
+ * Seedlist Discovery specification.
630
+ *
631
+ * @returns the suffix in lowercase A-label (Punycode) form, beginning with `.`
632
+ * @throws MongoParseError if the value is not a valid suffix
633
+ */
634
+ function normalizeSrvAllowedHostsSuffix(value: string): string {
635
+ // 1. Strip leading and trailing dots
636
+ const stripped = value.replace(/^\.+|\.+$/g, '');
637
+ if (stripped === '') {
638
+ throw new MongoParseError('srvAllowedHostsSuffix must contain at least one domain label');
639
+ }
640
+
641
+ // 2 and 3. Convert to lowercase A-label form, using the WHATWG URL Standard's domain parser, the
642
+ // same conversion applied to the host names returned by the SRV lookup
643
+ const suffix = dnsNameToASCII(stripped);
644
+ if (suffix == null) {
645
+ throw new MongoParseError(`srvAllowedHostsSuffix "${value}" is not a valid domain name`);
646
+ }
647
+
648
+ // 4. Require at least two labels, unless the value is a reserved single label
649
+ if (!suffix.includes('.') && !VALID_SINGLE_LABEL_SUFFIXES.has(suffix)) {
650
+ throw new MongoParseError(
651
+ `srvAllowedHostsSuffix "${value}" must contain at least two domain labels, or be one of: ${[
652
+ ...VALID_SINGLE_LABEL_SUFFIXES
653
+ ].join(', ')}`
654
+ );
655
+ }
656
+
657
+ // 5. Rejecting public suffixes via the Public Suffix List is a SHOULD that is intentionally not
658
+ // implemented by the Node Driver. Skipping.
659
+
660
+ // 6. Prepend a dot so the suffix only matches whole labels
661
+ return `.${suffix}`;
662
+ }
663
+
664
+ /** Ignored options are never validated, since their values have no effect. */
665
+ function isIgnoredOption(key: string): boolean {
666
+ return Object.entries(OPTIONS).some(
667
+ ([name, descriptor]) => descriptor.ignored && name.toLowerCase() === key.toLowerCase()
668
+ );
669
+ }
670
+
587
671
  function setOption(
588
672
  mongoOptions: any,
589
673
  key: string,
@@ -638,6 +722,8 @@ interface OptionDescriptor {
638
722
  default?: any;
639
723
 
640
724
  deprecated?: boolean | string;
725
+ /** The option is accepted but has no effect, so it is never stored on the resolved options. */
726
+ ignored?: boolean;
641
727
  /**
642
728
  * @param name - the original option name
643
729
  * @param options - the options so far for resolution
@@ -1105,10 +1191,40 @@ export const OPTIONS = {
1105
1191
  type: 'string'
1106
1192
  },
1107
1193
  socketTimeoutMS: {
1108
- // TODO(NODE-6491): deprecated: 'Please use timeoutMS instead',
1109
1194
  default: 0,
1110
1195
  type: 'uint'
1111
1196
  },
1197
+ srvAllowedHostsSuffix: {
1198
+ transform({ values: [value] }): string {
1199
+ if (typeof value !== 'string') {
1200
+ throw new MongoParseError('srvAllowedHostsSuffix must be a string');
1201
+ }
1202
+ return normalizeSrvAllowedHostsSuffix(value);
1203
+ }
1204
+ },
1205
+ srvHostValidator: {
1206
+ transform({ values: [value] }): (host: string) => boolean {
1207
+ if (typeof value !== 'function') {
1208
+ throw new MongoParseError('srvHostValidator must be a function');
1209
+ }
1210
+ // Wrap the validator so that every caller gets a boolean or a MongoAPIError
1211
+ return (host: string): boolean => {
1212
+ let isValid: unknown;
1213
+ try {
1214
+ isValid = value(host);
1215
+ } catch (error) {
1216
+ throw new MongoAPIError('srvHostValidator threw an error', { cause: error });
1217
+ }
1218
+ // A Promise (e.g. from an async validator) is truthy, so accepting it would silently allow every host
1219
+ if (typeof isValid !== 'boolean') {
1220
+ throw new MongoAPIError(
1221
+ `srvHostValidator must return a boolean, received ${typeof isValid}`
1222
+ );
1223
+ }
1224
+ return isValid;
1225
+ };
1226
+ }
1227
+ },
1112
1228
  srvMaxHosts: {
1113
1229
  type: 'uint',
1114
1230
  default: 0
@@ -1122,7 +1238,8 @@ export const OPTIONS = {
1122
1238
  type: 'boolean'
1123
1239
  },
1124
1240
  timeoutMS: {
1125
- type: 'uint'
1241
+ deprecated: 'timeoutMS has no effect and will be removed in a future release',
1242
+ ignored: true
1126
1243
  },
1127
1244
  tls: {
1128
1245
  type: 'boolean'
@@ -1176,7 +1293,6 @@ export const OPTIONS = {
1176
1293
  }
1177
1294
  },
1178
1295
  waitQueueTimeoutMS: {
1179
- // TODO(NODE-6491): deprecated: 'Please use timeoutMS instead',
1180
1296
  default: 0,
1181
1297
  type: 'uint'
1182
1298
  },