@juit/pgproxy-client-node 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,33 @@
1
+ # PostgreSQL Proxy Client (NodeJS Implementation)
2
+
3
+ This package provides a NodeJS specific client for PGProxy Servers.
4
+
5
+ * [Usage with PGClient](#usage-with-pgclient)
6
+ * [Direct Usage](#direct-usage)
7
+ * [PGProxy](https://github.com/juitnow/juit-pgproxy/blob/main/README.md)
8
+ * [Copyright Notice](https://github.com/juitnow/juit-pgproxy/blob/main/NOTICE.md)
9
+ * [License](https://github.com/juitnow/juit-pgproxy/blob/main/NOTICE.md)
10
+
11
+ ### Usage with PGClient
12
+
13
+ Simply register the client by importing it, and ensure that the `PGURL`
14
+ environment variable is set to the HTTP/HTTPS url of the server (or specify
15
+ the URL in the constructor):
16
+
17
+ ```ts
18
+ import '@juit/pgproxy-client-node'
19
+ import { PGClient } from '@juit/pgproxy-client'
20
+
21
+ const client = new PGClient('https://my-secret@my-pgproxy-server:54321/')
22
+ ```
23
+
24
+ ### Direct usage
25
+
26
+ The NodeJS client can be used directly by simply importing the `NodeClient`
27
+ class:
28
+
29
+ ```ts
30
+ import { NodeClient } from '@juit/pgproxy-client-node'
31
+
32
+ const client = new NodeClient('https://my-secret@my-pgproxy-server:54321/')
33
+ ```
package/dist/index.cjs ADDED
@@ -0,0 +1,139 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // index.ts
31
+ var src_exports = {};
32
+ __export(src_exports, {
33
+ NodeClient: () => NodeClient,
34
+ NodeProvider: () => NodeProvider
35
+ });
36
+ module.exports = __toCommonJS(src_exports);
37
+ var import_node_crypto = require("node:crypto");
38
+ var import_node_http = require("node:http");
39
+ var import_node_https = require("node:https");
40
+ var import_pgproxy_client = require("@juit/pgproxy-client");
41
+ var import_ws = __toESM(require("ws"));
42
+ function getAuthenticationToken(secret) {
43
+ const buffer = (0, import_node_crypto.randomBytes)(48);
44
+ buffer.writeBigInt64LE(BigInt(Date.now()), 0);
45
+ (0, import_node_crypto.createHmac)("sha256", Buffer.from(secret, "utf8")).update(buffer.subarray(0, 16)).digest().copy(buffer, 16);
46
+ return buffer.toString("base64");
47
+ }
48
+ function makeQuery(url, secret) {
49
+ const protocol = url.protocol === "https:" ? import_node_https.request : url.protocol === "http:" ? import_node_http.request : void 0;
50
+ (0, import_pgproxy_client.assert)(protocol, `Unsupported protocol "${url.protocol}"`);
51
+ const href = url.href;
52
+ return function query(query, params) {
53
+ const id = (0, import_node_crypto.randomUUID)();
54
+ return new Promise((resolve, reject) => {
55
+ const token = getAuthenticationToken(secret);
56
+ const url2 = new URL(href);
57
+ url2.searchParams.set("auth", token);
58
+ const req = protocol(url2, {
59
+ method: "POST",
60
+ headers: { "content-type": "application/json" }
61
+ }, (res) => {
62
+ const buffers = [];
63
+ res.on(
64
+ "error",
65
+ /* coverage ignore next */
66
+ (error) => reject(error)
67
+ );
68
+ res.on("data", (buffer2) => buffers.push(buffer2));
69
+ res.on("end", () => {
70
+ if (res.headers["content-type"] !== "application/json") {
71
+ return reject(new Error(`Invalid response (status=${res.statusCode})`));
72
+ } else {
73
+ const data = Buffer.concat(buffers).toString("utf-8");
74
+ return resolve(data);
75
+ }
76
+ });
77
+ });
78
+ const body = { id, query, params };
79
+ const buffer = JSON.stringify(body);
80
+ req.write(buffer);
81
+ req.end();
82
+ }).then((data) => {
83
+ let payload;
84
+ try {
85
+ payload = JSON.parse(data);
86
+ } catch (error) {
87
+ throw new Error("Unable to parse JSON payload");
88
+ }
89
+ (0, import_pgproxy_client.assert)(payload && typeof payload === "object", "JSON payload is not an object");
90
+ (0, import_pgproxy_client.assert)(payload.id === id, "Invalid/uncorrelated ID in response");
91
+ if (payload.statusCode === 200)
92
+ return payload;
93
+ throw new Error(`${payload.error || /* coverage ignore next */
94
+ "Unknown error"} (${payload.statusCode})`);
95
+ });
96
+ };
97
+ }
98
+ var NodeProvider = class extends import_pgproxy_client.WebSocketProvider {
99
+ constructor(url) {
100
+ super();
101
+ url = new URL(url.href);
102
+ const secret = decodeURIComponent(url.password || url.username);
103
+ (0, import_pgproxy_client.assert)(secret, "No connection secret specified in URL");
104
+ url.password = "";
105
+ url.username = "";
106
+ const baseHttpUrl = new URL(url.href);
107
+ const baseWsUrl = new URL(url.href);
108
+ baseWsUrl.protocol = `ws${baseWsUrl.protocol.slice(4)}`;
109
+ this._getUniqueRequestId = () => (0, import_node_crypto.randomUUID)();
110
+ this._getWebSocket = async () => {
111
+ const token = await getAuthenticationToken(secret);
112
+ const wsUrl = new URL(baseWsUrl.href);
113
+ wsUrl.searchParams.set("auth", token);
114
+ return this._connectWebSocket(new import_ws.default(wsUrl));
115
+ };
116
+ this.query = makeQuery(baseHttpUrl, secret);
117
+ }
118
+ /* ======================================================================== *
119
+ * METHODS FROM CONSTRUCTOR *
120
+ * ======================================================================== */
121
+ query;
122
+ _getWebSocket;
123
+ _getUniqueRequestId;
124
+ };
125
+ var NodeClient = class extends import_pgproxy_client.PGClient {
126
+ constructor(url) {
127
+ url = url || process.env.PGURL;
128
+ (0, import_pgproxy_client.assert)(url, "No URL to connect to (PGURL environment variable missing?)");
129
+ super(new NodeProvider(typeof url === "string" ? new URL(url) : url));
130
+ }
131
+ };
132
+ (0, import_pgproxy_client.registerProvider)("http", NodeProvider);
133
+ (0, import_pgproxy_client.registerProvider)("https", NodeProvider);
134
+ // Annotate the CommonJS export names for ESM import in node:
135
+ 0 && (module.exports = {
136
+ NodeClient,
137
+ NodeProvider
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": ["https", "http", "url", "buffer", "WebSocket"]
6
+ }
@@ -0,0 +1,13 @@
1
+ /// <reference types="node" />
2
+ import { PGClient, WebSocketProvider } from '@juit/pgproxy-client';
3
+ import WebSocket from 'ws';
4
+ import type { PGConnectionResult } from '@juit/pgproxy-client';
5
+ export declare class NodeProvider extends WebSocketProvider {
6
+ constructor(url: URL);
7
+ query: (query: string, params: (string | null)[]) => Promise<PGConnectionResult>;
8
+ protected _getWebSocket: () => Promise<WebSocket>;
9
+ protected _getUniqueRequestId: () => string;
10
+ }
11
+ export declare class NodeClient extends PGClient {
12
+ constructor(url?: URL | string);
13
+ }
package/dist/index.mjs ADDED
@@ -0,0 +1,103 @@
1
+ // index.ts
2
+ import { createHmac, randomBytes, randomUUID } from "node:crypto";
3
+ import { request as http } from "node:http";
4
+ import { request as https } from "node:https";
5
+ import { PGClient, WebSocketProvider, assert, registerProvider } from "@juit/pgproxy-client";
6
+ import WebSocket from "ws";
7
+ function getAuthenticationToken(secret) {
8
+ const buffer = randomBytes(48);
9
+ buffer.writeBigInt64LE(BigInt(Date.now()), 0);
10
+ createHmac("sha256", Buffer.from(secret, "utf8")).update(buffer.subarray(0, 16)).digest().copy(buffer, 16);
11
+ return buffer.toString("base64");
12
+ }
13
+ function makeQuery(url, secret) {
14
+ const protocol = url.protocol === "https:" ? https : url.protocol === "http:" ? http : void 0;
15
+ assert(protocol, `Unsupported protocol "${url.protocol}"`);
16
+ const href = url.href;
17
+ return function query(query, params) {
18
+ const id = randomUUID();
19
+ return new Promise((resolve, reject) => {
20
+ const token = getAuthenticationToken(secret);
21
+ const url2 = new URL(href);
22
+ url2.searchParams.set("auth", token);
23
+ const req = protocol(url2, {
24
+ method: "POST",
25
+ headers: { "content-type": "application/json" }
26
+ }, (res) => {
27
+ const buffers = [];
28
+ res.on(
29
+ "error",
30
+ /* coverage ignore next */
31
+ (error) => reject(error)
32
+ );
33
+ res.on("data", (buffer2) => buffers.push(buffer2));
34
+ res.on("end", () => {
35
+ if (res.headers["content-type"] !== "application/json") {
36
+ return reject(new Error(`Invalid response (status=${res.statusCode})`));
37
+ } else {
38
+ const data = Buffer.concat(buffers).toString("utf-8");
39
+ return resolve(data);
40
+ }
41
+ });
42
+ });
43
+ const body = { id, query, params };
44
+ const buffer = JSON.stringify(body);
45
+ req.write(buffer);
46
+ req.end();
47
+ }).then((data) => {
48
+ let payload;
49
+ try {
50
+ payload = JSON.parse(data);
51
+ } catch (error) {
52
+ throw new Error("Unable to parse JSON payload");
53
+ }
54
+ assert(payload && typeof payload === "object", "JSON payload is not an object");
55
+ assert(payload.id === id, "Invalid/uncorrelated ID in response");
56
+ if (payload.statusCode === 200)
57
+ return payload;
58
+ throw new Error(`${payload.error || /* coverage ignore next */
59
+ "Unknown error"} (${payload.statusCode})`);
60
+ });
61
+ };
62
+ }
63
+ var NodeProvider = class extends WebSocketProvider {
64
+ constructor(url) {
65
+ super();
66
+ url = new URL(url.href);
67
+ const secret = decodeURIComponent(url.password || url.username);
68
+ assert(secret, "No connection secret specified in URL");
69
+ url.password = "";
70
+ url.username = "";
71
+ const baseHttpUrl = new URL(url.href);
72
+ const baseWsUrl = new URL(url.href);
73
+ baseWsUrl.protocol = `ws${baseWsUrl.protocol.slice(4)}`;
74
+ this._getUniqueRequestId = () => randomUUID();
75
+ this._getWebSocket = async () => {
76
+ const token = await getAuthenticationToken(secret);
77
+ const wsUrl = new URL(baseWsUrl.href);
78
+ wsUrl.searchParams.set("auth", token);
79
+ return this._connectWebSocket(new WebSocket(wsUrl));
80
+ };
81
+ this.query = makeQuery(baseHttpUrl, secret);
82
+ }
83
+ /* ======================================================================== *
84
+ * METHODS FROM CONSTRUCTOR *
85
+ * ======================================================================== */
86
+ query;
87
+ _getWebSocket;
88
+ _getUniqueRequestId;
89
+ };
90
+ var NodeClient = class extends PGClient {
91
+ constructor(url) {
92
+ url = url || process.env.PGURL;
93
+ assert(url, "No URL to connect to (PGURL environment variable missing?)");
94
+ super(new NodeProvider(typeof url === "string" ? new URL(url) : url));
95
+ }
96
+ };
97
+ registerProvider("http", NodeProvider);
98
+ registerProvider("https", NodeProvider);
99
+ export {
100
+ NodeClient,
101
+ NodeProvider
102
+ };
103
+ //# 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": ["url", "buffer"]
6
+ }
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@juit/pgproxy-client-node",
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
+ "@types/ws": "^8.5.10",
39
+ "ws": "^8.15.1"
40
+ },
41
+ "directories": {
42
+ "test": "test"
43
+ },
44
+ "files": [
45
+ "*.md",
46
+ "dist/",
47
+ "src/"
48
+ ]
49
+ }
package/src/index.ts ADDED
@@ -0,0 +1,143 @@
1
+ import { createHmac, randomBytes, randomUUID } from 'node:crypto'
2
+ import { request as http } from 'node:http'
3
+ import { request as https } from 'node:https'
4
+
5
+ import { PGClient, WebSocketProvider, assert, registerProvider } from '@juit/pgproxy-client'
6
+ import WebSocket from 'ws'
7
+
8
+ import type { PGConnectionResult } from '@juit/pgproxy-client'
9
+ import type { Request, Response } from '@juit/pgproxy-server'
10
+
11
+
12
+ /** Create our authentication token */
13
+ function getAuthenticationToken(secret: string): string {
14
+ const buffer = randomBytes(48)
15
+
16
+ buffer.writeBigInt64LE(BigInt(Date.now()), 0)
17
+
18
+ createHmac('sha256', Buffer.from(secret, 'utf8'))
19
+ .update(buffer.subarray(0, 16))
20
+ .digest()
21
+ .copy(buffer, 16)
22
+
23
+ return buffer.toString('base64')
24
+ }
25
+
26
+ function makeQuery(url: URL, secret: string): (
27
+ query: string,
28
+ params: (string | null)[],
29
+ ) => Promise<PGConnectionResult> {
30
+ const protocol =
31
+ url.protocol === 'https:' ? https :
32
+ url.protocol === 'http:' ? http :
33
+ undefined
34
+ assert(protocol, `Unsupported protocol "${url.protocol}"`)
35
+ const href = url.href
36
+
37
+ return function query(
38
+ query: string,
39
+ params: (string | null)[],
40
+ ): Promise<PGConnectionResult> {
41
+ const id = randomUUID()
42
+
43
+ return new Promise<string>((resolve, reject) => {
44
+ const token = getAuthenticationToken(secret)
45
+ const url = new URL(href)
46
+ url.searchParams.set('auth', token)
47
+
48
+ const req = protocol(url, {
49
+ method: 'POST',
50
+ headers: { 'content-type': 'application/json' },
51
+ }, (res) => {
52
+ const buffers: Buffer[] = []
53
+ res.on('error', /* coverage ignore next */ (error) => reject(error))
54
+ res.on('data', (buffer) => buffers.push(buffer))
55
+ res.on('end', () => {
56
+ if (res.headers['content-type'] !== 'application/json') {
57
+ return reject(new Error(`Invalid response (status=${res.statusCode})`))
58
+ } else {
59
+ const data = Buffer.concat(buffers).toString('utf-8')
60
+ return resolve(data)
61
+ }
62
+ })
63
+ })
64
+
65
+ const body: Request = { id, query, params }
66
+ const buffer = JSON.stringify(body)
67
+ req.write(buffer)
68
+ req.end()
69
+ }).then((data: string) => {
70
+ let payload: Response
71
+ /* coverage ignore catch */
72
+ try {
73
+ payload = JSON.parse(data)
74
+ } catch (error) {
75
+ throw new Error('Unable to parse JSON payload')
76
+ }
77
+
78
+ /* Correlate our response to the request */
79
+ assert(payload && (typeof payload === 'object'), 'JSON payload is not an object')
80
+ assert(payload.id === id, 'Invalid/uncorrelated ID in response')
81
+
82
+ /* Analyze the _payload_ status code, is successful, we have a winner! */
83
+ if (payload.statusCode === 200) return payload
84
+ throw new Error(`${payload.error || /* coverage ignore next */ 'Unknown error'} (${payload.statusCode})`)
85
+ })
86
+ }
87
+ }
88
+
89
+ /* ========================================================================== *
90
+ * *
91
+ * ========================================================================== */
92
+
93
+ export class NodeProvider extends WebSocketProvider {
94
+ constructor(url: URL) {
95
+ super()
96
+
97
+ /* Clone the URL and verify it's http/https */
98
+ url = new URL(url.href)
99
+
100
+ /* Extract the secret from the url, we support both "http://secret@host/..."
101
+ * and/or "http://whomever:secret@host/..." formats, discarding username */
102
+ const secret = decodeURIComponent(url.password || url.username)
103
+ assert(secret, 'No connection secret specified in URL')
104
+ url.password = ''
105
+ url.username = ''
106
+
107
+ /* Prepare the URL for http and web sockets */
108
+ const baseHttpUrl = new URL(url.href)
109
+ const baseWsUrl = new URL(url.href)
110
+ baseWsUrl.protocol = `ws${baseWsUrl.protocol.slice(4)}`
111
+
112
+ /* Our methods */
113
+ this._getUniqueRequestId = (): string => randomUUID()
114
+
115
+ this._getWebSocket = async (): Promise<WebSocket> => {
116
+ const token = await getAuthenticationToken(secret)
117
+ const wsUrl = new URL(baseWsUrl.href)
118
+ wsUrl.searchParams.set('auth', token)
119
+ return this._connectWebSocket(new WebSocket(wsUrl))
120
+ }
121
+
122
+ this.query = makeQuery(baseHttpUrl, secret)
123
+ }
124
+
125
+ /* ======================================================================== *
126
+ * METHODS FROM CONSTRUCTOR *
127
+ * ======================================================================== */
128
+
129
+ query: (query: string, params: (string | null)[]) => Promise<PGConnectionResult>
130
+ protected _getWebSocket: () => Promise<WebSocket>
131
+ protected _getUniqueRequestId: () => string
132
+ }
133
+
134
+ export class NodeClient extends PGClient {
135
+ constructor(url?: URL | string) {
136
+ url = url || process.env.PGURL
137
+ assert(url, 'No URL to connect to (PGURL environment variable missing?)')
138
+ super(new NodeProvider(typeof url === 'string' ? new URL(url) : url))
139
+ }
140
+ }
141
+
142
+ registerProvider('http', NodeProvider)
143
+ registerProvider('https', NodeProvider)