Repository navigation
Expand file tree
/
Copy pathconnection-string-parser.ts
More file actions
1159 lines (1072 loc) · 54.1 KB
/
Copy pathconnection-string-parser.ts
File metadata and controls
1159 lines (1072 loc) · 54.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import { databendCloudHostWarehouse } from "@/lib/db/providers/sql/databend/cloud-host";
import {
isSnowflakeHost,
readSnowflakeAccount,
SNOWFLAKE_FIELD_SENTENCES,
} from "@/lib/db/providers/sql/snowflake/account-host";
import { DatabaseType, type SSLMode } from "@/lib/types";
export interface ParsedConnection {
type: DatabaseType;
host?: string;
port?: string;
user?: string;
password?: string;
database?: string;
connectionString?: string;
/**
* TLS the scheme asked for, when the fields alone could not express it.
*
* Set by every scheme that IS the transport - `https://` for ClickHouse,
* `rediss://`, `couchbases://` - and by their plaintext twins, because a paste
* overwrites rather than merges. Dropping it leaves the form on its "disable"
* default and the provider then speaks plaintext to a TLS port, which fails with a
* bare "fetch failed" / "Connection is closed." and nothing pointing at the scheme.
*
* `mongodb+srv://` is set too, to `verify-system`: the driver gets the URI verbatim and
* turns TLS on itself WITH chain verification, so that mode is a description of what
* happens rather than an instruction. It was deliberately left UNSET while `require`
* (rejectUnauthorized:false) was the only alternative, because the provider also sends
* an options object the driver prefers over the URI, and that would have stopped an
* Atlas certificate being verified. Couchbase used to be excused on the same
* "the provider re-reads the scheme" grounds, which was simply untrue: its transport
* is HTTP-only and derives http vs https from `config.ssl` alone.
*/
sslMode?: SSLMode;
/**
* A TLS parameter the pasted string carried that SSLMode cannot express, kept verbatim
* (`sslmode=prefer`, `ssl-mode=PREFERRED`, `Encrypt=Maybe`) so the form can say which one
* it declined to act on.
*
* Two classes end up here and both must leave `sslMode` untouched rather than fall to the
* form's "disable" default. The opportunistic values are a security decision, not a
* translation, and the measurements make the trap concrete: against postgres 18 with no
* server certificate `?sslmode=prefer` connects with `pg_stat_ssl.ssl = f` while
* `?sslmode=require` is refused outright ("server does not support SSL, but SSL was
* required"), so prefer -> require breaks a connection that works; against MySQL over TCP
* with its default self-signed certificate `--ssl-mode=PREFERRED` negotiates
* TLS_AES_128_GCM_SHA256 while DISABLED leaves Ssl_cipher empty, so PREFERRED -> disable
* silently downgrades a connection that was encrypted. The second class is any spelling the
* maps below do not know: guessing there is the same defect with less information.
*/
unmappedTLSParam?: string;
/**
* A MongoDB TLS parameter naming a file path (`tlsCAFile`, the only one reported so far,
* #842) rather than a value SSLMode could express, kept verbatim for the same reason
* `unmappedTLSParam` is.
*
* The driver reads this as a path on the machine running the server process, which in a
* container is not the machine the string was pasted on (measured: mongodb 7.6.0's
* `fs.readFile` throws ENOENT before any network traffic when the file is absent there).
* The parameter stays in the URI, and the form already has a place for the certificate's
* contents - the CA box under SSL / TLS, `ssl.caCert` in the connection config - which
* wins over it: the driver loads the file only when no `ca` option is set (`options.ca ??=`
* in mongo_client.js), so the fix is to point the user there. Kept separate from
* `unmappedTLSParam`: that field is documented around SSLMode's own value space, and a
* file path is not a value SSLMode could ever express.
*/
tlsFileParam?: string;
/**
* Set when a pasted MongoDB URI's credentials segment has more than one unescaped `@`,
* which makes the auth/host split ambiguous: `mongodb://user:p@ss@host/db` reads as
* password `p`, host `ss` (#842) instead of refusing. A properly percent-encoded password
* (`p%40ss`) never sets this - the encoded form has exactly one literal `@`.
*
* `user`, `password` and `host` are left unset rather than populated with the wrong
* guess; `connectionString` still carries the original paste unchanged.
*/
credentialsAmbiguous?: boolean;
/**
* The SqlClient protocol prefix of an ADO.NET `Server` that tedious cannot speak, kept as
* it was pasted (`np` for named pipes, `lpc` for shared memory). tedious reaches SQL
* Server over TCP only (#1211), so `host` and `port` are left unset rather than filled
* with `np:myserver`, which would only fail later as a misleading `ENOTFOUND`.
*/
unsupportedServerProtocol?: string;
/**
* The warehouse statements run on, for the form's Warehouse box: Databend's `warehouse=` (design 6.2), which
* Databend Cloud requires, or Snowflake's `warehouse=`, optional, the user's default warehouse when absent.
*/
warehouse?: string;
/** Snowflake's `schema=` or SQLAlchemy path schema, for the form's Schema box. */
schema?: string;
/** Snowflake's `role=`, the role every statement runs as. */
role?: string;
/** Snowflake's sign-in, from `authenticator=` (`SNOWFLAKE_JWT` or `PROGRAMMATIC_ACCESS_TOKEN`) or a `token=`. */
authMethod?: "keypair" | "pat";
/** Snowflake's private key passphrase, from `private_key_pwd`, `private_key_file_pwd` or `private_key_passphrase`. */
privateKeyPassphrase?: string;
/**
* The consent to send the password without TLS that the paste carries, for the form's box of that name. A Databend
* DSN has no parameter that gives it, so a Databend paste answers `false`, and the form clears a consent an earlier
* connection or paste left ticked rather than send the password in cleartext to a host nobody consented for. Absent
* for every other scheme, whose paste leaves the box as it was: Db2's form shares it.
*/
allowInsecureAuth?: boolean;
/**
* The names of the query parameters the paste did not apply, each once, in the order pasted. Names only: a
* parameter's value can be a secret, so it never leaves the parser.
*/
ignoredParameters?: string[];
/**
* How many other query parameters the paste did not apply, counted and never named: a parameter that is not one of
* the driver's own names, or a text with no `name=`, can be the rest of a value an unencoded `&` split off.
*/
unnamedParameters?: number;
/**
* Why the paste was refused as a whole, in the sentence the form shows. When set, nothing but `type` and
* `discardPaste` is, and the form applies nothing, its type included unless `discardPaste` is set.
*/
refusal?: string;
/**
* Set with a refusal whose text may hold a secret, a Snowflake token or key among them: the form clears the paste
* box rather than keep the text there to be corrected, and switches to `type`, filling nothing, so the box the
* sentence names is in view.
*/
discardPaste?: true;
/**
* A sentence the form shows beside a successful paste, for a parameter that was applied with a meaning the field
* alone does not show.
*/
notice?: string;
/**
* Sentences the form shows as a warning beside a paste that filled the other fields, each for a TLS parameter the
* paste read and did not apply, in the scheme's own words, since the form's own TLS sentences describe its modes and
* not the scheme's.
*/
cautions?: string[];
}
/**
* The canonical URI scheme each engine is reachable by, for the engines that HAVE one.
*
* Deliberately partial, and the gaps are the point: SQLite is a file path, LibreDB is
* embedded in the process, and Druid is reached over plain HTTP through the form. Inventing
* `sqlite://` or `druid://` to make the map look complete would put a scheme on the login
* page that `parseConnectionString` below rejects - a claim the product does not honour,
* which is the class of defect issue #425 exists to remove.
*
* Cassandra is absent for a different reason again, and a stronger one: its driver needs
* a REQUIRED `localDataCenter` alongside the contact points (measured on 4.9.0 - it
* refuses to construct a client without one), and no URI convention in use carries that
* field. A `cassandra://host:9042/keyspace` form would therefore parse into a connection
* that cannot open, which is worse than no paste at all.
*
* Trino is the sharpest version of that gap, and worth naming because it looks like it
* belongs here: it HAS a canonical URL, `jdbc:trino://host:port/catalog/schema`. That is
* a JDBC URL and not a URI - `new URL()` reads its scheme as `jdbc:` - and stripping the
* prefix to make one would invent a `trino://` form no Trino tool emits. So the id is
* absent, its capabilities say `supportsConnectionString: false`, and its form offers no
* paste toggle. Pinned by tests/unit/lib/connection-string-parser.test.ts.
*
* Aliases the parser also accepts (`postgresql://`, `mongodb+srv://`, `rediss://`,
* `sqlserver://`, `couchbases://`, and ClickHouse's `http(s)://`) are not listed: this map
* answers "what is the one scheme to show a reader for this engine", not "what will parse".
*
* `tests/unit/lib/connection-string-parser.test.ts` pins the entries against the parser
* itself - every scheme here must round-trip to its own `DatabaseType` - so the map cannot
* drift away from the `startsWith` checks below without failing CI.
*/
export const ENGINE_URI_SCHEMES: Partial<Record<DatabaseType, string>> = {
postgres: "postgres",
mysql: "mysql",
mongodb: "mongodb",
redis: "redis",
oracle: "oracle",
mssql: "mssql",
db2: "db2",
couchbase: "couchbase",
clickhouse: "clickhouse",
libsql: "libsql",
databend: "databend",
};
/**
* The sentences a refused Databend paste shows (design 6.2), one per refusal. Each refusal fills no field, so the
* sentence says what to paste instead.
*/
export const DATABEND_DSN_REFUSALS = Object.freeze({
fragment: "The DSN contains #, which ends a URL: percent-encode it as %23, or type the password in its own field.",
userinfo:
"The DSN has an @ after a / or ?, which end the address part of a URL: percent-encode a /, ? or @ in the user or password as %2F, %3F or %40, or type the password in its own field, and an @ in the path or a parameter as %40.",
signIn:
"Token and key-pair sign-in are not supported in this version: paste a DSN that signs in with a SQL user and password, or fill the fields.",
flight: "Flight SQL (port 8900) is not supported: paste the HTTP DSN, databend://, for port 8000 or 443.",
jdbc: "This is a JDBC URL, which reads TLS and ports differently (TLS off and port 8000 by default). Paste the databend:// DSN from Connect, or fill the fields.",
shellExport: "Paste the DSN itself: the databend:// text inside the quotes.",
});
/**
* What a Databend DSN's `sslmode=require` and `sslmode=enable` mean (X34): BendSQL connects over https with the
* certificate verified, which is this form's verify-system and not its unverified require.
*/
export const DATABEND_SSLMODE_NOTICES = Object.freeze({
require: "sslmode=require in a Databend DSN verifies the certificate, as BendSQL does, so SSL mode is verify-system.",
enable: "sslmode=enable in a Databend DSN verifies the certificate, as BendSQL does, so SSL mode is verify-system.",
});
/**
* The cautions of a Databend paste (`cautions`), for the TLS parameters it reads and does not apply: a CA file path,
* which only the machine running BendSQL could read, and an sslmode BendSQL does not read, which the form's own TLS
* sentence would list among the modes it says the parameter has no equivalent in.
*/
export const DATABEND_DSN_CAUTIONS = Object.freeze({
caFile:
"Studio reads no CA file path from a DSN, so tls_ca_file was not applied: paste the certificate's contents into the CA field under SSL / TLS.",
sslmode: (mode: string) =>
`sslmode=${mode} is not a Databend DSN mode (BendSQL reads disable, require and enable), so SSL mode was left as it was: choose one under SSL / TLS.`,
});
/**
* The warning a Databend paste shows for the parameters it did not apply: BendSQL's own names, never valued, and a
* count of the rest, which are never named.
*/
export function databendNotAppliedNotice(names: readonly string[], unnamed = 0): string {
const counted = `${unnamed} ${names.length > 0 ? "other " : ""}parameter${unnamed === 1 ? "" : "s"}`;
const listed = [names.join(", "), unnamed > 0 ? counted : ""].filter(Boolean).join(" and ");
return `Not applied: ${listed}. Studio's Databend connection takes host, port, user, password, database, warehouse and TLS; the other fields were filled in.`;
}
/** The sentences of a Snowflake paste: two refusals, each naming the parameter in this list's own spelling. */
export const SNOWFLAKE_PASTE_SENTENCES = Object.freeze({
inlineKey: (name: string) =>
`Studio does not take ${name} from a pasted URL. Paste the key itself into Private key, not inside a URL.`,
path: (name: string) =>
`Studio does not take ${name} from a pasted URL. Studio reads no file: paste the key's contents into Private key.`,
tls: "Studio checks TLS whatever insecureMode or disableOCSPChecks say.",
});
/**
* Parse a database connection string into its components, or null when no form below reads it.
* Schemes, matched case-sensitively on the trimmed input: postgres://, postgresql://, mysql://,
* mongodb://, mongodb+srv://, rediss://, redis://, oracle://, mssql://, sqlserver://, db2://,
* couchbases://, couchbase://, libsql://, clickhouse://, and http:// and https:// (both ClickHouse),
* databend://, databend+http:// and databend+https://, plus the ADO.NET form `Server=...;` (SQL Server), whose
* `Server=` key is matched case-insensitively. Databend's other spellings (`databend+flight://`, `databend+grpc://`,
* `jdbc:databend://` and a `BENDSQL_DSN=` shell line) parse to a refusal.
*/
export function parseConnectionString(input: string): ParsedConnection | null {
const trimmed = input.trim();
if (!trimmed) return null;
// Databend (design 6.2). The refusals first: each names a Databend spelling Studio does not connect with.
if (/^(export\s+)?BENDSQL_DSN=/.test(trimmed)) return databendRefusal("shellExport");
if (trimmed.startsWith("jdbc:databend://")) return databendRefusal("jdbc");
if (trimmed.startsWith("databend+flight://") || trimmed.startsWith("databend+grpc://")) {
return databendRefusal("flight");
}
if (/^databend(\+https?)?:\/\//.test(trimmed)) return parseDatabendDSN(trimmed);
// Snowflake, ahead of ClickHouse's https:// rule: only an exact Snowflake host or Snowsight is claimed.
if (isSnowflakePaste(trimmed)) return parseSnowflakeURL(trimmed);
// MongoDB connection strings
if (trimmed.startsWith("mongodb://") || trimmed.startsWith("mongodb+srv://")) {
return parseMongoDBString(trimmed);
}
// PostgreSQL
if (trimmed.startsWith("postgres://") || trimmed.startsWith("postgresql://")) {
return parseGenericURL(trimmed, "postgres", "5432");
}
// MySQL
if (trimmed.startsWith("mysql://")) {
return parseGenericURL(trimmed, "mysql", "3306");
}
// Redis - `rediss://` IS TLS (the secure form in Redis's own URI convention,
// the one `redis-cli --tls` and every client library reads that way) and ioredis only
// negotiates it when a `tls` option is present, which the provider builds from
// `config.ssl` alone. `require` rather than a verifying mode: it maps to
// `rejectUnauthorized: false`, and a self-hosted Redis presents a self-signed
// certificate by default, so a verifying mode would refuse the ordinary
// `--tls-port` deployment. A paste therefore encrypts but never silently claims to
// have checked a chain; the panel is where verification is chosen. The plain scheme
// is the explicit plaintext twin, so it clears a mode the form is still holding.
if (trimmed.startsWith("rediss://")) {
return withSSLMode(parseGenericURL(trimmed, "redis", "6379"), "require");
}
if (trimmed.startsWith("redis://")) {
return withSSLMode(parseGenericURL(trimmed, "redis", "6379"), "disable");
}
// Oracle
if (trimmed.startsWith("oracle://")) {
return parseGenericURL(trimmed, "oracle", "1521");
}
// MSSQL / SQL Server
if (trimmed.startsWith("mssql://") || trimmed.startsWith("sqlserver://")) {
return parseGenericURL(trimmed, "mssql", "1433");
}
// Db2 LUW (#786). The TLS it asks for rides in the query string, read by `readQueryTLS`.
if (trimmed.startsWith("db2://")) {
return parseGenericURL(trimmed, "db2", "50000");
}
// Couchbase — the TLS scheme is checked first, it is not a prefix of the plain one.
// The mode travels for the same reason as ClickHouse's: this provider talks HTTP,
// and `CouchbaseHttpTransport` picks `https` vs `http` from `config.ssl`, never from
// the pasted string it also stores. Without the mode a `couchbases://` paste posts
// plain HTTP to 18091. `require` for the same reason as Redis: Capella's certificate
// is CA-signed, a self-hosted cluster's is not, and only the panel should turn
// verification on.
if (trimmed.startsWith("couchbases://")) {
return withSSLMode(parseCouchbaseString(trimmed, "18091"), "require");
}
if (trimmed.startsWith("couchbase://")) {
return withSSLMode(parseCouchbaseString(trimmed, "8091"), "disable");
}
// ClickHouse — the HTTP endpoint IS the connection target, so a bare http(s) URL is
// as canonical as the clickhouse:// scheme and no other provider claims those
// schemes. 8123 rather than 9000 because the provider speaks HTTP, never the native
// protocol the CLI's clickhouse:// URIs point at; https:// defaults to the server's
// https_port instead. The result is field-based (no connectionString), so the pasted
// URL never has to be re-parsed at connect time.
// The three schemes differ in what they say about TLS, and each has to be able to
// OVERWRITE a mode the form already carries - pasting is not a merge:
// clickhouse:// names no transport -> say nothing, defer to the form
// http:// explicitly plaintext -> disable, or a stale "require" survives
// https:// explicitly TLS -> require, or a Cloud endpoint gets plain HTTP
// libSQL. TLS always, and the token rides in the query string rather than in the
// authority: `libsql://<database>-<org>.turso.io?authToken=<jwt>` is what Turso's
// own CLI prints, and `libsql://` has no plaintext form - a self-hosted server on
// plain HTTP is reached through the host/port fields with TLS off, because
// `http://` already resolves to ClickHouse here and two engines cannot own one
// scheme.
if (trimmed.startsWith("libsql://")) {
return parseLibSQLURL(trimmed);
}
if (trimmed.startsWith("clickhouse://")) {
return parseGenericURL(trimmed, "clickhouse", "8123");
}
if (trimmed.startsWith("http://")) {
return withSSLMode(parseGenericURL(trimmed, "clickhouse", "8123"), "disable");
}
if (trimmed.startsWith("https://")) {
return withSSLMode(parseGenericURL(trimmed, "clickhouse", "8443"), "require");
}
// ADO.NET format: Server=host;Database=db;User Id=user;Password=pass;
if (/^Server\s*=/i.test(trimmed)) {
return parseADONetString(trimmed);
}
return null;
}
/**
* What a TLS parameter resolved to: a mode the form can hold, or the parameter itself when
* it has no representation. Never both, and never a fallback to "disable".
*/
interface TLSIntent {
sslMode?: SSLMode;
unmappedTLSParam?: string;
}
/**
* libpq's sslmode, minus `prefer` and `allow`. Those two are absent on purpose (see the
* `unmappedTLSParam` note): they mean "encrypt if the server offers it", which no SSLMode
* value describes. `verify-system` is not in the table either - it is this form's own mode
* name and not a libpq one, so a string carrying it is a string we cannot honour.
*/
const POSTGRES_SSLMODE: Record<string, SSLMode> = {
disable: "disable",
require: "require",
"verify-ca": "verify-ca",
"verify-full": "verify-full",
};
/**
* MySQL's --ssl-mode, minus PREFERRED for the same reason. VERIFY_IDENTITY checks the
* hostname as well as the chain, which is what verify-full means here.
*/
const MYSQL_SSL_MODE: Record<string, SSLMode> = {
disabled: "disable",
required: "require",
verify_ca: "verify-ca",
verify_identity: "verify-full",
};
/**
* Db2's `security=` keyword: `SSL` is the one value that names a TLS transport (#786). It maps to
* a VERIFYING mode, by D26's rule and because the Db2 provider fails closed on unverified
* transport: without TLS the password crosses the network in cleartext.
*/
const DB2_SECURITY: Record<string, SSLMode> = { ssl: "verify-system" };
/** Db2's boolean `ssl=`, both ends mappable, onto the mode `security=SSL` reads as (#786). */
const DB2_SSL: Record<string, SSLMode> = {
true: "verify-system",
"1": "verify-system",
false: "disable",
"0": "disable",
};
/**
* One query parameter kept in the spelling it was pasted in. The lookup is
* case-insensitive, but the banner quotes the parameter back at the user, so what it echoes
* has to be their text and not a normalised form of it.
*/
interface RawParam {
name: string;
value: string;
}
/**
* Map a documented value case-insensitively, or hand back the parameter verbatim.
*
* `Object.hasOwn` rather than a bare lookup: the tables are object literals, so an
* all-lowercase Object.prototype key reaches them. `?sslmode=constructor` resolved to the
* Object constructor FUNCTION - truthy, so it was written into `sslMode` and set as the
* form's SSLMode, smuggling a non-SSLMode value past the banner this function exists to
* raise. Pinned by tests/unit/lib/connection-string-parser.test.ts.
*/
function mapTLSValue(param: RawParam, table: Record<string, SSLMode>): TLSIntent {
const key = param.value.toLowerCase();
return Object.hasOwn(table, key) ? { sslMode: table[key] } : { unmappedTLSParam: `${param.name}=${param.value}` };
}
/**
* The boolean TLS spellings: postgres's JDBC/Heroku `ssl=true`, MySQL's `ssl` / `useSSL`
* (Connector/J's pre-`sslMode` switch, still written by several ORMs) and MongoDB's `tls` /
* `ssl`. A boolean has no opportunistic value, so unlike `sslmode`/`ssl-mode` both ends of it
* are mappable.
*
* THE RULE (D26): a boolean TLS spelling is mapped onto the mode that matches what the
* engine's OWN driver does with it, and never onto a weaker one. `true` therefore maps to
* `verify-system`, because that is what the drivers do: `pg` given `ssl=true` and mysql2 given
* `ssl: {}` both connect with Node's default `rejectUnauthorized: true`, and the mongodb
* driver reads `tls=true` as TLS with chain verification. `false` maps to `disable`, which is
* the same rule read the other way.
*
* `require` used to be the answer for `true`, and was wrong in one direction while the mongodb
* refusal was wrong in the other: `require` is `rejectUnauthorized: false` in every provider
* that has the knob, so a paste that asked for a verified connection got an unverified one,
* and there was no mode to map onto that did not also demand a CA PEM the user does not have.
* `verify-system` (src/lib/types.ts) is that mode, so the rule above no longer has to trade
* security against completability.
*/
function readBooleanTLS(param: RawParam): TLSIntent {
const flag = param.value.toLowerCase();
if (flag === "true" || flag === "1") return { sslMode: "verify-system" };
if (flag === "false" || flag === "0") return { sslMode: "disable" };
return { unmappedTLSParam: `${param.name}=${param.value}` };
}
/**
* The MongoDB URI's TLS options, read for both schemes.
*
* `tls=true` (and the driver's deprecated `ssl=` alias) goes through the boolean rule above,
* and `mongodb+srv://` with no TLS parameter carries the same mode: the driver turns TLS on
* for every SRV connection itself, WITH verification, so leaving the form on its `disable`
* default described a connection that is in fact encrypted as one that is not.
*
* `tlsInsecure` / `tlsAllowInvalidCertificates` pull the result back down to `require`. Both
* turn `rejectUnauthorized` off while leaving TLS on (mongodb.d.ts documents the mapping onto
* the Node names), which is exactly what `require` means here - and the provider sends its
* options object as a second channel the driver prefers over the URI, so a verifying mode
* would break a string that connects today. `tlsAllowInvalidHostnames` is NOT in that set: it
* relaxes the name check only, which our options object never sets, so the URI's own
* relaxation survives alongside a verifying mode.
*/
function readMongoTLS(uri: string): TLSIntent {
const params = new Map<string, RawParam>();
const queryStart = uri.indexOf("?");
if (queryStart >= 0) {
for (const [name, value] of new URLSearchParams(uri.slice(queryStart + 1))) {
params.set(name.toLowerCase(), { name, value });
}
}
const flag = params.get("tls") ?? params.get("ssl");
const intent: TLSIntent = flag
? readBooleanTLS(flag)
: uri.startsWith("mongodb+srv://")
? { sslMode: "verify-system" }
: {};
if (intent.sslMode === undefined || intent.sslMode === "disable") return intent;
const relax = params.get("tlsinsecure") ?? params.get("tlsallowinvalidcertificates");
if (relax !== undefined && relax.value.toLowerCase() === "true") return { sslMode: "require" };
// `tlsCAFile` pins the chain to a CA the URI names rather than the system store, which is
// `verify-ca`: the mode that shows the CA box the paste banner points at (#842).
return params.has("tlscafile") ? { sslMode: "verify-ca" } : intent;
}
/**
* MongoDB TLS parameters that name a file path rather than a value SSLMode could express.
* `tlsCAFile` is the only one reported so far (#842, via AWS DocumentDB's own connection
* string) - scoped to it rather than the driver's full file-path option set until another
* is actually seen.
*/
function readMongoTLSFileParam(uri: string): string | undefined {
const queryStart = uri.indexOf("?");
if (queryStart < 0) return undefined;
for (const [name, value] of new URLSearchParams(uri.slice(queryStart + 1))) {
if (name.toLowerCase() === "tlscafile") return `${name}=${value}`;
}
return undefined;
}
/**
* ADO.NET's Encrypt/TrustServerCertificate pair, from either the keyword string or an
* mssql:// query. Encrypt on with TrustServerCertificate on is `require`: encrypted, chain
* unchecked. Encrypt on with it off - including absent, which is the documented default -
* validates the chain AND the name, so verify-full. `Strict` is TDS 8.0, where the driver
* ignores TrustServerCertificate entirely and always validates.
*
* An absent Encrypt says nothing rather than "disable": System.Data.SqlClient defaults it to
* false and Microsoft.Data.SqlClient 4.0+ to true, so the string alone does not carry the answer.
*/
function readADONetTLS(get: (key: string) => string | undefined): TLSIntent {
const encrypt = get("encrypt");
if (encrypt === undefined) return {};
const value = encrypt.toLowerCase();
if (value === "false" || value === "no") return { sslMode: "disable" };
if (value === "strict") return { sslMode: "verify-full" };
if (value !== "true" && value !== "yes") return { unmappedTLSParam: `Encrypt=${encrypt}` };
const trust = get("trustservercertificate");
if (trust === undefined) return { sslMode: "verify-full" };
const trusted = trust.toLowerCase();
if (trusted === "true" || trusted === "yes") return { sslMode: "require" };
if (trusted === "false" || trusted === "no") return { sslMode: "verify-full" };
return { unmappedTLSParam: `TrustServerCertificate=${trust}` };
}
/**
* The TLS a URL carries in its QUERY STRING, for the engines that put it there.
*
* Only postgres, mysql, mssql and db2 are read here: for `rediss://`, `couchbases://` and
* ClickHouse's `http(s)://` the scheme IS the transport and already decided, and MongoDB's
* URI is read by `readMongoTLS`, which parses it by hand because `mongodb+srv://` is not a
* URL this function's caller can build.
*
* Postgres's sslrootcert/sslcert/sslkey are read by nobody here on purpose: they name files on
* the machine that wrote the string, while the form holds PEM text and the process that opens
* the connection is the server, which cannot read that path.
*/
function readQueryTLS(url: URL, type: DatabaseType): TLSIntent {
const params = new Map<string, RawParam>();
for (const [name, value] of url.searchParams) params.set(name.toLowerCase(), { name, value });
if (type === "postgres") {
// The explicit mode wins over the boolean: a string carrying both said the specific
// thing on purpose.
const sslmode = params.get("sslmode");
if (sslmode) return mapTLSValue(sslmode, POSTGRES_SSLMODE);
const ssl = params.get("ssl");
return ssl ? readBooleanTLS(ssl) : {};
}
if (type === "mysql") {
const mode = params.get("ssl-mode") ?? params.get("sslmode");
if (mode) return mapTLSValue(mode, MYSQL_SSL_MODE);
// Reading only ssl-mode dropped `?ssl=true` and `?useSSL=true` with no mode and no
// banner - the silent downgrade the whole TLS read exists to prevent. mysql2 also
// accepts an OBJECT here (`ssl={"rejectUnauthorized":true}`); that is not a boolean, so
// it falls through to the banner rather than being guessed at.
const flag = params.get("ssl") ?? params.get("usessl");
return flag ? readBooleanTLS(flag) : {};
}
if (type === "mssql") return readADONetTLS((key) => params.get(key)?.value);
if (type === "db2") {
// `security=SSL` is the keyword Db2's own CLI and JDBC drivers read; `ssl` is the boolean
// spelling other URLs carry. Both ask for a verifying mode: see `DB2_SECURITY`.
const security = params.get("security");
if (security) return mapTLSValue(security, DB2_SECURITY);
const ssl = params.get("ssl");
return ssl ? mapTLSValue(ssl, DB2_SSL) : {};
}
return {};
}
/**
* Attach a scheme's TLS intent, preserving the null a malformed URL parses to. The mode
* overwrites whatever the form was holding - pasting is not a merge.
*
* A scheme is read more weakly than the boolean *parameter* beside it, and that is a
* decision rather than an artefact (D28). D26 settled the rule for a boolean parameter:
* it maps onto the mode matching what the engine's own driver does with it, never onto a
* weaker one. A scheme cannot answer to that rule, because the secure scheme is the only
* spelling a self-hosted deployment has: `rediss://`, `couchbases://` and ClickHouse's
* `https://` all describe servers that present a self-signed certificate by default, so
* a verifying mode here would refuse the ordinary `--tls-port` install with a chain
* error. `require` therefore encrypts without ever claiming to have checked a chain, and
* the connection panel stays the one place verification is turned on. The per-scheme
* reason is recorded at each call site above; this is the rule they share.
*/
function withSSLMode(parsed: ParsedConnection | null, sslMode: SSLMode): ParsedConnection | null {
return parsed && { ...parsed, sslMode };
}
function parseMongoDBString(uri: string): ParsedConnection {
const tlsFileParam = readMongoTLSFileParam(uri);
const result: ParsedConnection = {
type: "mongodb",
connectionString: uri,
...readMongoTLS(uri),
...(tlsFileParam ? { tlsFileParam } : {}),
};
try {
// For mongodb+srv, we can't use URL directly for host/port
// but we can extract user/pass/database
const isSRV = uri.startsWith("mongodb+srv://");
// Extract database from path
const withoutProtocol = uri.replace(/^mongodb(\+srv)?:\/\//, "");
// The authority (credentials + host) ends at the first "/" or "?", where the driver's
// own grammar ends it, so an "@" in an option value is not counted. More than one
// literal "@" in it means an unescaped reserved character in the credentials -
// `p@ss:w#rd@host` - makes the auth/host split ambiguous, so refuse to guess rather
// than mis-split it (#842). The driver refuses the same string as "Invalid connection
// string". Percent-encoded credentials (`p%40ss`) never trigger this: the encoded form
// has exactly one literal "@".
const authorityEnd = withoutProtocol.search(/[/?]/);
const authority = authorityEnd >= 0 ? withoutProtocol.slice(0, authorityEnd) : withoutProtocol;
if ((authority.match(/@/g) ?? []).length > 1) {
result.credentialsAmbiguous = true;
return result;
}
const atIndex = withoutProtocol.indexOf("@");
const afterAuth = atIndex >= 0 ? withoutProtocol.slice(atIndex + 1) : withoutProtocol;
// Split host(s) from path
const slashIndex = afterAuth.indexOf("/");
if (slashIndex >= 0) {
const pathPart = afterAuth.slice(slashIndex + 1);
const dbName = pathPart.split("?")[0];
if (dbName) result.database = decodeURIComponent(dbName);
}
// Extract credentials
if (atIndex >= 0) {
const authPart = withoutProtocol.slice(0, atIndex);
const colonIndex = authPart.indexOf(":");
if (colonIndex >= 0) {
result.user = decodeURIComponent(authPart.slice(0, colonIndex));
result.password = decodeURIComponent(authPart.slice(colonIndex + 1));
} else {
result.user = decodeURIComponent(authPart);
}
}
// Extract host/port for non-SRV
if (!isSRV && slashIndex >= 0) {
const hostPart = afterAuth.slice(0, slashIndex);
const firstHost = hostPart.split(",")[0]; // take first host for replica sets
const [host, port] = firstHost.split(":");
if (host) result.host = host;
if (port) result.port = port;
}
} catch {
// If parsing fails, we still have the connectionString
}
return result;
}
/**
* Percent-decode a component, keeping it verbatim when it is not a valid escape
* sequence. Couchbase bucket names may legally contain a literal "%".
*/
export function safeDecodeURIComponent(value: string): string {
try {
return decodeURIComponent(value);
} catch {
return value;
}
}
/**
* Parse a Couchbase connection string. Only the management port is stored — 8091 for
* couchbase://, 18091 for couchbases:// — because the query ports are discovered from the
* cluster at connect time. A Capella endpoint (couchbases://cb.<id>.cloud.couchbase.com)
* carries neither a port nor a bucket path, so both stay at their defaults and no bucket
* is invented. The full original string is always preserved.
*/
function parseCouchbaseString(uri: string, defaultPort: string): ParsedConnection | null {
try {
const url = new URL(uri);
// Multi-node connection strings list hosts comma-separated; take the first one.
const [firstHost] = url.hostname.split(",");
const bucket = url.pathname.slice(1); // remove leading /
return {
type: "couchbase",
host: firstHost || "localhost",
port: url.port || defaultPort,
user: url.username ? decodeURIComponent(url.username) : undefined,
password: url.password ? decodeURIComponent(url.password) : undefined,
database: bucket ? safeDecodeURIComponent(bucket) : undefined,
connectionString: uri,
};
} catch {
return null;
}
}
function parseADONetString(input: string): ParsedConnection | null {
try {
const params: Record<string, string> = {};
input.split(";").forEach((part) => {
const eq = part.indexOf("=");
if (eq > 0) {
const key = part.slice(0, eq).trim().toLowerCase();
const val = part.slice(eq + 1).trim();
params[key] = val;
}
});
const host = params["server"] || params["data source"] || "localhost";
const rest = {
user: params["user id"] || params["uid"] || undefined,
password: params["password"] || params["pwd"] || undefined,
database: params["database"] || params["initial catalog"] || undefined,
...readADONetTLS((key) => params[key]),
};
// SqlClient accepts a protocol prefix on the server, and the Azure portal always
// writes one (`tcp:<server>.database.windows.net,1433`). tedious resolves the value
// as a hostname, so a kept prefix fails as ENOTFOUND. TCP is the only protocol
// tedious speaks: `tcp:` is dropped, `np:` and `lpc:` are reported instead of guessed.
const protocol = /^(tcp|np|lpc):/i.exec(host);
if (protocol && protocol[1].toLowerCase() !== "tcp") {
return { type: "mssql", unsupportedServerProtocol: protocol[1], ...rest };
}
const [hostPart, portPart] = host.slice(protocol ? protocol[0].length : 0).split(",");
return {
type: "mssql",
host: hostPart || "localhost",
port: portPart || "1433",
...rest,
};
} catch {
return null;
}
}
/**
* A `libsql://` URL as a connection.
*
* Three things differ from `parseGenericURL` and each is measured against what Turso
* emits: the credential is `?authToken=`, so the URL's own password slot is only a
* fallback; there is no database in the path, because the database IS the hostname;
* and the default port is 443 under required TLS, which is how Turso Cloud serves
* every database.
*/
function parseLibSQLURL(uri: string): ParsedConnection | null {
try {
const url = new URL(uri);
return {
type: "libsql",
host: url.hostname || "localhost",
port: url.port || "443",
password: url.searchParams.get("authToken") ?? (url.password ? decodeURIComponent(url.password) : undefined),
sslMode: "require",
};
} catch {
return null;
}
}
function parseGenericURL(uri: string, type: DatabaseType, defaultPort: string): ParsedConnection | null {
try {
const url = new URL(uri);
return {
type,
host: url.hostname || "localhost",
port: url.port || defaultPort,
user: url.username ? decodeURIComponent(url.username) : undefined,
password: url.password ? decodeURIComponent(url.password) : undefined,
database: url.pathname.slice(1) || undefined, // remove leading /
...readQueryTLS(url, type),
};
} catch {
return null;
}
}
function databendRefusal(key: keyof typeof DATABEND_DSN_REFUSALS): ParsedConnection {
return { type: "databend", refusal: DATABEND_DSN_REFUSALS[key] };
}
/** The DSN parameters a paste reads, each into a field or a caution; every other name is reported as not applied. */
const DATABEND_APPLIED_PARAMETERS = new Set(["warehouse", "sslmode", "tls_ca_file"]);
/**
* The other parameters BendSQL reads by name (`core/src/client.rs`, `from_dsn`), which a paste names when it does not
* apply them. BendSQL sends any other name as a session setting; a paste counts those and never names them, since an
* unencoded `&` inside a value makes the rest of the value read as a name.
*/
const DATABEND_NAMED_PARAMETERS = new Set([
"wait_time_secs",
"max_rows_in_buffer",
"max_rows_per_page",
"connect_timeout",
"page_request_timeout_secs",
"presign",
"tenant",
"role",
"login",
"session_token",
"body_format",
"query_result_format",
"retry_count",
"retry_delay_secs",
]);
/**
* The query parameters a Databend paste did not apply: BendSQL's own names, each once in the order pasted, and how
* many others there are. A text with no `=` is counted even when it spells a name, since it can be a value's tail.
*/
function databendIgnoredParameters(search: string): { names: string[]; unnamed: number } {
const names = new Set<string>();
const unnamed = new Set<string>();
for (const pair of search.slice(1).split("&")) {
if (pair === "") continue;
const equals = pair.indexOf("=");
if (equals < 0) {
unnamed.add(pair);
continue;
}
const name = [...new URLSearchParams(pair).keys()][0];
if (DATABEND_APPLIED_PARAMETERS.has(name)) continue;
if (DATABEND_NAMED_PARAMETERS.has(name)) names.add(name);
else unnamed.add(name);
}
return { names: [...names], unnamed: unnamed.size };
}
/** The `sslmode` values BendSQL reads; any other refuses the DSN there. */
const DATABEND_SSLMODES = new Set(["disable", "require", "enable"]);
/** BendSQL's sign-in parameters other than a password, which this version does not support. */
const DATABEND_SIGN_IN_PARAMETERS = new Set([
"access_token",
"access_token_file",
"private_key_file",
"private_key_passphrase_file",
]);
/**
* A Databend DSN, read the way BendSQL reads it (`core/src/client.rs`, `from_dsn`): TLS unless `sslmode=disable`,
* where `require` and `enable` verify the certificate; the DSN's port, else 443 with TLS and 80 without; the path is
* the database and `warehouse=` the warehouse. `databend+http://` and `databend+https://` set the transport as
* databend-go reads them, and an explicit `sslmode` wins over either; one BendSQL does not read leaves SSL mode as it
* was, with a caution, and the port the scheme's. A repeated parameter takes its last value, as BendSQL's does
* (databend-go takes the first).
*
* Three departures, each refusing rather than guessing: a `#` ends a URL, so a password holding one would be cut short;
* a `/` or `?` ends the address part, so with one in the user or the password the URL parser reads the rest of the
* password as the database or a parameter, and since an `@` past the address part can also be a path's or a
* parameter's, and a password can hold an unencoded `@` before its `/`, every such `@` is refused, naming each
* encoding; and BendSQL's token and key-pair sign-in have no field here. Unlike BendSQL, which decodes the password
* only, the user is percent-decoded too, as every other scheme here does. With no `warehouse=`, the warehouse an older Databend Cloud host names
* (`<tenant>--<warehouse>.gw...`) fills Warehouse.
*/
function parseDatabendDSN(uri: string): ParsedConnection | null {
if (uri.includes("#")) return databendRefusal("fragment");
// An @ past the address part reads more than one way: the sign-in's own, after a / or ? in the user or password; a
// later @ of a password that also holds one, as `root:p@ss/x@host` does; or an unencoded @ of the path or a
// parameter. An @ inside the address part settles none of them, so every such text is refused.
const address = uri.slice(uri.indexOf("://") + 3);
const addressEnd = address.search(/[/?]/);
if (addressEnd >= 0 && address.includes("@", addressEnd)) return databendRefusal("userinfo");
let url: URL;
try {
url = new URL(uri);
} catch {
return null;
}
const names = [...new Set(url.searchParams.keys())];
if (names.some((name) => DATABEND_SIGN_IN_PARAMETERS.has(name))) return databendRefusal("signIn");
// A repeated parameter takes its last value, as BendSQL's loop over the pairs overwrites, and an sslmode it
// cannot read refuses the DSN wherever it stands.
const sslmodes = url.searchParams.getAll("sslmode");
const sslmode = sslmodes.find((mode) => !DATABEND_SSLMODES.has(mode)) ?? sslmodes.at(-1) ?? null;
const plainScheme = uri.startsWith("databend+http://");
let tls: TLSIntent = { sslMode: plainScheme ? "disable" : "verify-system" };
let notice: string | undefined;
const cautions: string[] = [];
if (sslmode === "disable") tls = { sslMode: "disable" };
else if (sslmode === "require" || sslmode === "enable") {
tls = { sslMode: "verify-system" };
notice = DATABEND_SSLMODE_NOTICES[sslmode];
} else if (sslmode !== null) {
tls = {};
cautions.push(DATABEND_DSN_CAUTIONS.sslmode(sslmode));
}
// BendSQL reads the CA file on its own machine; Studio reads no path, and its CA box takes the certificate itself.
if (url.searchParams.has("tls_ca_file")) cautions.push(DATABEND_DSN_CAUTIONS.caFile);
const warehouse = url.searchParams.getAll("warehouse").at(-1) || databendCloudHostWarehouse(url.hostname);
const ignored = databendIgnoredParameters(url.search);
// An sslmode BendSQL does not read leaves the mode unset, so the port is the scheme's: 80 for +http, else 443.
const plain = tls.sslMode === undefined ? plainScheme : tls.sslMode === "disable";
return {
type: "databend",
host: url.hostname || "localhost",
port: url.port || (plain ? "80" : "443"),
user: url.username ? safeDecodeURIComponent(url.username) : undefined,
password: url.password ? safeDecodeURIComponent(url.password) : undefined,
database: url.pathname.slice(1) || undefined,
...(warehouse ? { warehouse } : {}),
...tls,
allowInsecureAuth: false,
...(notice ? { notice } : {}),
...(cautions.length > 0 ? { cautions } : {}),
...(ignored.names.length > 0 ? { ignoredParameters: ignored.names } : {}),
...(ignored.unnamed > 0 ? { unnamedParameters: ignored.unnamed } : {}),
};
}
/**
* Detect the database type from a connection string.
*/
export function detectConnectionStringType(input: string): DatabaseType | null {
const trimmed = input.trim().toLowerCase();
if (trimmed.startsWith("postgres://") || trimmed.startsWith("postgresql://")) return "postgres";
if (trimmed.startsWith("mysql://")) return "mysql";
if (trimmed.startsWith("mongodb://") || trimmed.startsWith("mongodb+srv://")) return "mongodb";
if (trimmed.startsWith("redis://") || trimmed.startsWith("rediss://")) return "redis";
if (trimmed.startsWith("oracle://")) return "oracle";
if (trimmed.startsWith("mssql://") || trimmed.startsWith("sqlserver://")) return "mssql";
if (trimmed.startsWith("db2://")) return "db2";
if (trimmed.startsWith("couchbase://") || trimmed.startsWith("couchbases://")) return "couchbase";
if (trimmed.startsWith("libsql://")) return "libsql";
if (/^databend(\+https?)?:\/\//.test(trimmed)) return "databend";
if (isSnowflakePaste(input.trim())) return "snowflake";
if (trimmed.startsWith("clickhouse://") || trimmed.startsWith("http://") || trimmed.startsWith("https://"))
return "clickhouse";
if (/^server\s*=/i.test(trimmed)) return "mssql";
return null;
}
// ---------------------------------------------------------------------------------------
// Snowflake
// ---------------------------------------------------------------------------------------
const SNOWFLAKE_SCHEMES = ["jdbc:snowflake://", "snowflake://"] as const;
const SNOWSIGHT_HOST = "app.snowflake.com";
/** The host of an `https://` text, lower-cased, or undefined when it is not a URL. */
function httpsHost(text: string): string | undefined {
if (!text.toLowerCase().startsWith("https://")) return undefined;
try {
return new URL(text).hostname.toLowerCase();
} catch {
return undefined;
}
}
/**
* Whether the paste is Snowflake's: a `jdbc:snowflake://` or `snowflake://` URL, an `https://` URL whose host is a
* Snowflake host or Snowsight, or a bare Snowflake host. An identifier with no suffix is not claimed: it would claim
* any word, so it is typed in Account instead.
*/
function isSnowflakePaste(text: string): boolean {
if (SNOWFLAKE_SCHEMES.some((scheme) => text.startsWith(scheme))) return true;
const host = httpsHost(text);
if (host !== undefined) return host === SNOWSIGHT_HOST || isSnowflakeHost(host);
return !/[\s/:?#@]/.test(text) && isSnowflakeHost(text);
}
/** Applied to their own fields, value percent-decoded once. */
const SNOWFLAKE_FIELD_PARAMETERS: Readonly<Record<string, "user" | "warehouse" | "database" | "schema" | "role">> = {
user: "user",
warehouse: "warehouse",
db: "database",
database: "database",
schema: "schema",
role: "role",
};
const SNOWFLAKE_PASSPHRASE_PARAMETERS = new Set(["private_key_pwd", "private_key_file_pwd", "private_key_passphrase"]);