@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 +34 -0
- package/dist/index.cjs +139 -0
- package/dist/index.cjs.map +6 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.mjs +113 -0
- package/dist/index.mjs.map +6 -0
- package/package.json +47 -0
- package/src/index.ts +182 -0
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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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)
|