@juit/pgproxy-client-whatwg 1.0.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.
package/README.md ADDED
@@ -0,0 +1,34 @@
1
+ # PostgreSQL Proxy Client (WHATWG + WebCrypto Implementation)
2
+
3
+ This package provides client for PGProxy Servers based on WHATWG `fetch` and
4
+ WebSockets, and the WebCrypto API.
5
+
6
+ * [Usage with PGClient](#usage-with-pgclient)
7
+ * [Direct Usage](#direct-usage)
8
+ * [PGProxy](https://github.com/juitnow/juit-pgproxy/blob/main/README.md)
9
+ * [Copyright Notice](https://github.com/juitnow/juit-pgproxy/blob/main/NOTICE.md)
10
+ * [License](https://github.com/juitnow/juit-pgproxy/blob/main/NOTICE.md)
11
+
12
+ ### Usage with PGClient
13
+
14
+ Simply register the client by importing it, and ensure that the `PGURL`
15
+ environment variable is set to the HTTP/HTTPS url of the server (or specify
16
+ the URL in the constructor):
17
+
18
+ ```ts
19
+ import '@juit/pgproxy-client-whatwg'
20
+ import { PGClient } from '@juit/pgproxy-client'
21
+
22
+ const client = new PGClient('https://my-secret@my-pgproxy-server:54321/')
23
+ ```
24
+
25
+ ### Direct usage
26
+
27
+ The WHATWG client can be used directly by simply importing the `WHATWGClient`
28
+ class:
29
+
30
+ ```ts
31
+ import { WHATWGClient } from '@juit/pgproxy-client-whatwg'
32
+
33
+ const client = new WHATWGClient('https://my-secret@my-pgproxy-server:54321/')
34
+ ```
package/dist/index.cjs ADDED
@@ -0,0 +1,139 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // index.ts
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
+ WHATWGClient: () => WHATWGClient,
24
+ WHATWGProvider: () => WHATWGProvider
25
+ });
26
+ module.exports = __toCommonJS(src_exports);
27
+ var import_pgproxy_client = require("@juit/pgproxy-client");
28
+ async function createToken(secret, crypto = globalThis.crypto) {
29
+ const encoder = new TextEncoder();
30
+ const buffer = new ArrayBuffer(48);
31
+ const token = new Uint8Array(buffer);
32
+ crypto.getRandomValues(token);
33
+ const timestamp = new DataView(buffer, 0, 8);
34
+ timestamp.setBigInt64(0, BigInt(Date.now()), true);
35
+ const header = new Uint8Array(buffer, 0, 16);
36
+ const key = await crypto.subtle.importKey(
37
+ "raw",
38
+ // ........................ // Our key type
39
+ encoder.encode(secret),
40
+ // ....... // UTF-8 representation of the secret
41
+ { name: "HMAC", hash: "SHA-256" },
42
+ // We want the HMAC(SHA-256)
43
+ false,
44
+ // ........................ // The key is not exportable
45
+ ["sign", "verify"]
46
+ );
47
+ const signature = await crypto.subtle.sign(
48
+ "HMAC",
49
+ // ............. // We need an HMAC
50
+ key,
51
+ // ................ // Use the key as allocated above
52
+ header
53
+ );
54
+ token.set(new Uint8Array(signature), 16);
55
+ const string = String.fromCharCode(...token);
56
+ return btoa(string).replaceAll("+", "-").replaceAll("/", "_");
57
+ }
58
+ var WHATWGProvider = class _WHATWGProvider extends import_pgproxy_client.WebSocketProvider {
59
+ constructor(url, options = {}) {
60
+ super();
61
+ const {
62
+ WebSocket = _WHATWGProvider.WebSocket,
63
+ crypto = _WHATWGProvider.crypto,
64
+ fetch = _WHATWGProvider.fetch
65
+ } = options;
66
+ url = new URL(url);
67
+ (0, import_pgproxy_client.assert)(/^https?:$/.test(url.protocol), `Unsupported protocol "${url.protocol}"`);
68
+ const secret = decodeURIComponent(url.password || url.username);
69
+ (0, import_pgproxy_client.assert)(secret, "No connection secret specified in URL");
70
+ url.password = "";
71
+ url.username = "";
72
+ const baseHttpUrl = new URL(url);
73
+ const baseWsUrl = new URL(url);
74
+ baseWsUrl.protocol = `ws${baseWsUrl.protocol.slice(4)}`;
75
+ this._getUniqueRequestId = () => crypto.randomUUID();
76
+ this._getWebSocket = async () => {
77
+ const token = await createToken(secret, crypto);
78
+ const wsUrl = new URL(baseWsUrl);
79
+ wsUrl.searchParams.set("auth", token);
80
+ return this._connectWebSocket(new WebSocket(wsUrl));
81
+ };
82
+ this.query = async (query, params) => {
83
+ const token = await createToken(secret, crypto);
84
+ const httpUrl = new URL(baseHttpUrl);
85
+ httpUrl.searchParams.set("auth", token);
86
+ const id = crypto.randomUUID();
87
+ const response = await fetch(httpUrl, {
88
+ method: "POST",
89
+ headers: { "content-type": "application/json" },
90
+ body: JSON.stringify({ id, query, params })
91
+ });
92
+ if (response.headers.get("content-type") !== "application/json") {
93
+ throw new Error(`Invalid response (status=${response.status})`);
94
+ }
95
+ let payload;
96
+ try {
97
+ payload = await response.json();
98
+ } catch (error) {
99
+ throw new Error(`Unable to parse JSON payload (status=${response.status})`);
100
+ }
101
+ (0, import_pgproxy_client.assert)(payload && typeof payload === "object", "JSON payload is not an object");
102
+ (0, import_pgproxy_client.assert)(payload.id === id, 'Invalid/uncorrelated ID in response"');
103
+ if (payload.statusCode === 200)
104
+ return payload;
105
+ throw new Error(`${payload.error || /* coverage ignore next */
106
+ "Unknown error"} (${payload.statusCode})`);
107
+ };
108
+ }
109
+ /* ======================================================================== *
110
+ * METHODS FROM CONSTRUCTOR *
111
+ * ======================================================================== */
112
+ query;
113
+ _getWebSocket;
114
+ _getUniqueRequestId;
115
+ /* ======================================================================== *
116
+ * ENVIRONMENT OVERRIDES *
117
+ * ======================================================================== */
118
+ /** Constructor for {@link WebSocket} instances (default: `globalThis.WebSocket`) */
119
+ static WebSocket = globalThis.WebSocket;
120
+ /** Web Cryptography API implementation (default: `globalThis.crypto`) */
121
+ static crypto = globalThis.crypto;
122
+ /** WHATWG `fetch` implementation (default: `globalThis.fetch`) */
123
+ static fetch = globalThis.fetch;
124
+ };
125
+ var WHATWGClient = class extends import_pgproxy_client.PGClient {
126
+ constructor(url) {
127
+ url = url || globalThis.process?.env?.PGURL;
128
+ (0, import_pgproxy_client.assert)(url, "No URL to connect to (PGURL environment variable missing?)");
129
+ super(new WHATWGProvider(typeof url === "string" ? new URL(url) : url));
130
+ }
131
+ };
132
+ (0, import_pgproxy_client.registerProvider)("http", WHATWGProvider);
133
+ (0, import_pgproxy_client.registerProvider)("https", WHATWGProvider);
134
+ // Annotate the CommonJS export names for ESM import in node:
135
+ 0 && (module.exports = {
136
+ WHATWGClient,
137
+ WHATWGProvider
138
+ });
139
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1,6 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/index.ts"],
4
+ "mappings": "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",
5
+ "names": []
6
+ }
@@ -0,0 +1,34 @@
1
+ import { PGClient, WebSocketProvider } from '@juit/pgproxy-client';
2
+ import type { PGConnectionResult, PGWebSocket } from '@juit/pgproxy-client';
3
+ type MimimalCrypto = {
4
+ getRandomValues: Crypto['getRandomValues'];
5
+ randomUUID: Crypto['randomUUID'];
6
+ subtle: {
7
+ importKey: SubtleCrypto['importKey'];
8
+ sign: SubtleCrypto['sign'];
9
+ };
10
+ };
11
+ type MimimalWebSocket = {
12
+ new (url: URL): PGWebSocket;
13
+ };
14
+ export interface WHATWGOptions {
15
+ WebSocket?: typeof globalThis.WebSocket;
16
+ crypto?: MimimalCrypto;
17
+ fetch?: typeof globalThis.fetch;
18
+ }
19
+ export declare class WHATWGProvider extends WebSocketProvider {
20
+ constructor(url: URL, options?: WHATWGOptions);
21
+ query: (query: string, params: (string | null)[]) => Promise<PGConnectionResult>;
22
+ protected _getWebSocket: () => Promise<PGWebSocket>;
23
+ protected _getUniqueRequestId: () => string;
24
+ /** Constructor for {@link WebSocket} instances (default: `globalThis.WebSocket`) */
25
+ static WebSocket: MimimalWebSocket;
26
+ /** Web Cryptography API implementation (default: `globalThis.crypto`) */
27
+ static crypto: MimimalCrypto;
28
+ /** WHATWG `fetch` implementation (default: `globalThis.fetch`) */
29
+ static fetch: typeof fetch;
30
+ }
31
+ export declare class WHATWGClient extends PGClient {
32
+ constructor(url?: URL | string);
33
+ }
34
+ export {};
package/dist/index.mjs ADDED
@@ -0,0 +1,113 @@
1
+ // index.ts
2
+ import { PGClient, WebSocketProvider, assert, registerProvider } from "@juit/pgproxy-client";
3
+ async function createToken(secret, crypto = globalThis.crypto) {
4
+ const encoder = new TextEncoder();
5
+ const buffer = new ArrayBuffer(48);
6
+ const token = new Uint8Array(buffer);
7
+ crypto.getRandomValues(token);
8
+ const timestamp = new DataView(buffer, 0, 8);
9
+ timestamp.setBigInt64(0, BigInt(Date.now()), true);
10
+ const header = new Uint8Array(buffer, 0, 16);
11
+ const key = await crypto.subtle.importKey(
12
+ "raw",
13
+ // ........................ // Our key type
14
+ encoder.encode(secret),
15
+ // ....... // UTF-8 representation of the secret
16
+ { name: "HMAC", hash: "SHA-256" },
17
+ // We want the HMAC(SHA-256)
18
+ false,
19
+ // ........................ // The key is not exportable
20
+ ["sign", "verify"]
21
+ );
22
+ const signature = await crypto.subtle.sign(
23
+ "HMAC",
24
+ // ............. // We need an HMAC
25
+ key,
26
+ // ................ // Use the key as allocated above
27
+ header
28
+ );
29
+ token.set(new Uint8Array(signature), 16);
30
+ const string = String.fromCharCode(...token);
31
+ return btoa(string).replaceAll("+", "-").replaceAll("/", "_");
32
+ }
33
+ var WHATWGProvider = class _WHATWGProvider extends WebSocketProvider {
34
+ constructor(url, options = {}) {
35
+ super();
36
+ const {
37
+ WebSocket = _WHATWGProvider.WebSocket,
38
+ crypto = _WHATWGProvider.crypto,
39
+ fetch = _WHATWGProvider.fetch
40
+ } = options;
41
+ url = new URL(url);
42
+ assert(/^https?:$/.test(url.protocol), `Unsupported protocol "${url.protocol}"`);
43
+ const secret = decodeURIComponent(url.password || url.username);
44
+ assert(secret, "No connection secret specified in URL");
45
+ url.password = "";
46
+ url.username = "";
47
+ const baseHttpUrl = new URL(url);
48
+ const baseWsUrl = new URL(url);
49
+ baseWsUrl.protocol = `ws${baseWsUrl.protocol.slice(4)}`;
50
+ this._getUniqueRequestId = () => crypto.randomUUID();
51
+ this._getWebSocket = async () => {
52
+ const token = await createToken(secret, crypto);
53
+ const wsUrl = new URL(baseWsUrl);
54
+ wsUrl.searchParams.set("auth", token);
55
+ return this._connectWebSocket(new WebSocket(wsUrl));
56
+ };
57
+ this.query = async (query, params) => {
58
+ const token = await createToken(secret, crypto);
59
+ const httpUrl = new URL(baseHttpUrl);
60
+ httpUrl.searchParams.set("auth", token);
61
+ const id = crypto.randomUUID();
62
+ const response = await fetch(httpUrl, {
63
+ method: "POST",
64
+ headers: { "content-type": "application/json" },
65
+ body: JSON.stringify({ id, query, params })
66
+ });
67
+ if (response.headers.get("content-type") !== "application/json") {
68
+ throw new Error(`Invalid response (status=${response.status})`);
69
+ }
70
+ let payload;
71
+ try {
72
+ payload = await response.json();
73
+ } catch (error) {
74
+ throw new Error(`Unable to parse JSON payload (status=${response.status})`);
75
+ }
76
+ assert(payload && typeof payload === "object", "JSON payload is not an object");
77
+ assert(payload.id === id, 'Invalid/uncorrelated ID in response"');
78
+ if (payload.statusCode === 200)
79
+ return payload;
80
+ throw new Error(`${payload.error || /* coverage ignore next */
81
+ "Unknown error"} (${payload.statusCode})`);
82
+ };
83
+ }
84
+ /* ======================================================================== *
85
+ * METHODS FROM CONSTRUCTOR *
86
+ * ======================================================================== */
87
+ query;
88
+ _getWebSocket;
89
+ _getUniqueRequestId;
90
+ /* ======================================================================== *
91
+ * ENVIRONMENT OVERRIDES *
92
+ * ======================================================================== */
93
+ /** Constructor for {@link WebSocket} instances (default: `globalThis.WebSocket`) */
94
+ static WebSocket = globalThis.WebSocket;
95
+ /** Web Cryptography API implementation (default: `globalThis.crypto`) */
96
+ static crypto = globalThis.crypto;
97
+ /** WHATWG `fetch` implementation (default: `globalThis.fetch`) */
98
+ static fetch = globalThis.fetch;
99
+ };
100
+ var WHATWGClient = class extends PGClient {
101
+ constructor(url) {
102
+ url = url || globalThis.process?.env?.PGURL;
103
+ assert(url, "No URL to connect to (PGURL environment variable missing?)");
104
+ super(new WHATWGProvider(typeof url === "string" ? new URL(url) : url));
105
+ }
106
+ };
107
+ registerProvider("http", WHATWGProvider);
108
+ registerProvider("https", WHATWGProvider);
109
+ export {
110
+ WHATWGClient,
111
+ WHATWGProvider
112
+ };
113
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1,6 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/index.ts"],
4
+ "mappings": "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",
5
+ "names": []
6
+ }
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@juit/pgproxy-client-whatwg",
3
+ "version": "1.0.0",
4
+ "main": "./dist/index.cjs",
5
+ "module": "./dist/index.mjs",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "require": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.cjs"
12
+ },
13
+ "import": {
14
+ "types": "./dist/index.d.ts",
15
+ "default": "./dist/index.mjs"
16
+ }
17
+ }
18
+ },
19
+ "author": "Juit Developers <developers@juit.com>",
20
+ "license": "Apache-2.0",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+ssh://git@github.com/juitnow/juit-pgproxy.git"
24
+ },
25
+ "keywords": [
26
+ "database",
27
+ "pg",
28
+ "pool",
29
+ "postgres",
30
+ "proxy"
31
+ ],
32
+ "bugs": {
33
+ "url": "https://github.com/juitnow/juit-pgproxy/issues"
34
+ },
35
+ "homepage": "https://github.com/juitnow/juit-pgproxy#readme",
36
+ "dependencies": {
37
+ "@juit/pgproxy-client": "1.0.0"
38
+ },
39
+ "directories": {
40
+ "test": "test"
41
+ },
42
+ "files": [
43
+ "*.md",
44
+ "dist/",
45
+ "src/"
46
+ ]
47
+ }
package/src/index.ts ADDED
@@ -0,0 +1,182 @@
1
+ import { PGClient, WebSocketProvider, assert, registerProvider } from '@juit/pgproxy-client'
2
+
3
+ import type { PGConnectionResult, PGWebSocket } from '@juit/pgproxy-client'
4
+ import type { Request, Response } from '@juit/pgproxy-server'
5
+
6
+ type MimimalCrypto = {
7
+ getRandomValues: Crypto['getRandomValues']
8
+ randomUUID: Crypto['randomUUID']
9
+ subtle: {
10
+ importKey: SubtleCrypto['importKey']
11
+ sign: SubtleCrypto['sign']
12
+ }
13
+ }
14
+
15
+ type MimimalWebSocket = {
16
+ new (url: URL): PGWebSocket
17
+ }
18
+
19
+ /* ========================================================================== *
20
+ * INTERNALS *
21
+ * ========================================================================== */
22
+
23
+ async function createToken(
24
+ secret: string,
25
+ crypto: MimimalCrypto = globalThis.crypto,
26
+ ): Promise<string> {
27
+ const encoder = new TextEncoder()
28
+
29
+ /* Prepare the buffer and its Uint8Array view for the token */
30
+ const buffer = new ArrayBuffer(48)
31
+ const token = new Uint8Array(buffer)
32
+
33
+ /* Fill the whole token with random data */
34
+ crypto.getRandomValues(token)
35
+
36
+ /* Write the timestamp at offset 0 as a little endian 64-bits bigint */
37
+ const timestamp = new DataView(buffer, 0, 8)
38
+ timestamp.setBigInt64(0, BigInt(Date.now()), true)
39
+
40
+ /* Prepare the message, concatenating the header and database name */
41
+ const header = new Uint8Array(buffer, 0, 16)
42
+
43
+ /* Prepare the key for HMAC-SHA-256 */
44
+ const key = await crypto.subtle.importKey(
45
+ 'raw', // ........................ // Our key type
46
+ encoder.encode(secret), // ....... // UTF-8 representation of the secret
47
+ { name: 'HMAC', hash: 'SHA-256' }, // We want the HMAC(SHA-256)
48
+ false, // ........................ // The key is not exportable
49
+ [ 'sign', 'verify' ]) // ......... // Key is used to sign and verify
50
+
51
+ /* Compute the signature of the message using the key */
52
+ const signature = await crypto.subtle.sign(
53
+ 'HMAC', // ............. // We need an HMAC
54
+ key, // ................ // Use the key as allocated above
55
+ header) // ............ // The message to sign, as UTF-8
56
+
57
+ /* Copy the signature into our token */
58
+ token.set(new Uint8Array(signature), 16)
59
+
60
+ /* Encode the token as an URL-safe BASE-64 string */
61
+ const string = String.fromCharCode(...token)
62
+ return btoa(string)
63
+ .replaceAll('+', '-')
64
+ .replaceAll('/', '_')
65
+ }
66
+
67
+ /* ========================================================================== *
68
+ * WHATWG PROVIDER AND CLIENT IMPLEMENTATION *
69
+ * ========================================================================== */
70
+
71
+ export interface WHATWGOptions {
72
+ WebSocket?: typeof globalThis.WebSocket,
73
+ crypto?: MimimalCrypto, // typeof globalThis.crypto,
74
+ fetch?: typeof globalThis.fetch,
75
+ }
76
+
77
+ export class WHATWGProvider extends WebSocketProvider {
78
+ constructor(url: URL, options: WHATWGOptions = {}) {
79
+ super()
80
+
81
+ const {
82
+ WebSocket = WHATWGProvider.WebSocket,
83
+ crypto = WHATWGProvider.crypto,
84
+ fetch = WHATWGProvider.fetch,
85
+ } = options
86
+
87
+ /* Clone the URL and verify it's http/https */
88
+ url = new URL(url)
89
+ assert(/^https?:$/.test(url.protocol), `Unsupported protocol "${url.protocol}"`)
90
+
91
+ /* Extract the secret from the url, we support both "http://secret@host/..."
92
+ * and/or "http://whomever:secret@host/..." formats, discarding username */
93
+ const secret = decodeURIComponent(url.password || url.username)
94
+ assert(secret, 'No connection secret specified in URL')
95
+ url.password = ''
96
+ url.username = ''
97
+
98
+ /* Prepare the URL for http and web sockets */
99
+ const baseHttpUrl = new URL(url)
100
+ const baseWsUrl = new URL(url)
101
+ baseWsUrl.protocol = `ws${baseWsUrl.protocol.slice(4)}`
102
+
103
+ /* Our methods */
104
+ this._getUniqueRequestId = (): string => crypto.randomUUID()
105
+
106
+ this._getWebSocket = async (): Promise<PGWebSocket> => {
107
+ const token = await createToken(secret, crypto)
108
+ const wsUrl = new URL(baseWsUrl)
109
+ wsUrl.searchParams.set('auth', token)
110
+ return this._connectWebSocket(new WebSocket(wsUrl))
111
+ }
112
+
113
+ this.query = async (
114
+ query: string,
115
+ params: (string | null)[],
116
+ ): Promise<PGConnectionResult> => {
117
+ const token = await createToken(secret, crypto)
118
+ const httpUrl = new URL(baseHttpUrl)
119
+ httpUrl.searchParams.set('auth', token)
120
+
121
+ /* Get a fresh ID to correlate requests and responses */
122
+ const id = crypto.randomUUID()
123
+
124
+ /* Fetch out our request (let errors fall through) */
125
+ const response = await fetch(httpUrl, {
126
+ method: 'POST',
127
+ headers: { 'content-type': 'application/json' },
128
+ body: JSON.stringify({ id, query, params } satisfies Request),
129
+ })
130
+
131
+ if (response.headers.get('content-type') !== 'application/json') {
132
+ throw new Error(`Invalid response (status=${response.status})`)
133
+ }
134
+
135
+ let payload: Response
136
+ /* coverage ignore catch */
137
+ try {
138
+ payload = await response.json()
139
+ } catch (error) {
140
+ throw new Error(`Unable to parse JSON payload (status=${response.status})`)
141
+ }
142
+
143
+ /* Correlate the response to the request */
144
+ assert(payload && (typeof payload === 'object'), 'JSON payload is not an object')
145
+ assert(payload.id === id, 'Invalid/uncorrelated ID in response"')
146
+
147
+ /* Analyze the _payload_ status code, is successful, we have a winner! */
148
+ if (payload.statusCode === 200) return payload
149
+ throw new Error(`${payload.error || /* coverage ignore next */ 'Unknown error'} (${payload.statusCode})`)
150
+ }
151
+ }
152
+
153
+ /* ======================================================================== *
154
+ * METHODS FROM CONSTRUCTOR *
155
+ * ======================================================================== */
156
+
157
+ query: (query: string, params: (string | null)[]) => Promise<PGConnectionResult>
158
+ protected _getWebSocket: () => Promise<PGWebSocket>
159
+ protected _getUniqueRequestId: () => string
160
+
161
+ /* ======================================================================== *
162
+ * ENVIRONMENT OVERRIDES *
163
+ * ======================================================================== */
164
+
165
+ /** Constructor for {@link WebSocket} instances (default: `globalThis.WebSocket`) */
166
+ static WebSocket: MimimalWebSocket = globalThis.WebSocket
167
+ /** Web Cryptography API implementation (default: `globalThis.crypto`) */
168
+ static crypto: MimimalCrypto = globalThis.crypto
169
+ /** WHATWG `fetch` implementation (default: `globalThis.fetch`) */
170
+ static fetch = globalThis.fetch
171
+ }
172
+
173
+ export class WHATWGClient extends PGClient {
174
+ constructor(url?: URL | string) {
175
+ url = url || (globalThis as any).process?.env?.PGURL
176
+ assert(url, 'No URL to connect to (PGURL environment variable missing?)')
177
+ super(new WHATWGProvider(typeof url === 'string' ? new URL(url) : url))
178
+ }
179
+ }
180
+
181
+ registerProvider('http', WHATWGProvider)
182
+ registerProvider('https', WHATWGProvider)