@onreza/sqlx-js 0.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/LICENSE +21 -0
- package/README.md +496 -0
- package/ROADMAP.md +28 -0
- package/dist/bin/sqlx-js.d.ts +2 -0
- package/dist/bin/sqlx-js.js +180 -0
- package/dist/src/bun-runtime.d.ts +7 -0
- package/dist/src/bun-runtime.js +45 -0
- package/dist/src/bun.d.ts +22 -0
- package/dist/src/bun.js +9 -0
- package/dist/src/cache.d.ts +36 -0
- package/dist/src/cache.js +104 -0
- package/dist/src/codegen.d.ts +2 -0
- package/dist/src/codegen.js +74 -0
- package/dist/src/commands/migrate.d.ts +63 -0
- package/dist/src/commands/migrate.js +283 -0
- package/dist/src/commands/prepare.d.ts +23 -0
- package/dist/src/commands/prepare.js +314 -0
- package/dist/src/commands/schema.d.ts +14 -0
- package/dist/src/commands/schema.js +72 -0
- package/dist/src/commands/watch.d.ts +21 -0
- package/dist/src/commands/watch.js +94 -0
- package/dist/src/config.d.ts +6 -0
- package/dist/src/config.js +23 -0
- package/dist/src/index.d.ts +23 -0
- package/dist/src/index.js +10 -0
- package/dist/src/pg/analyze.d.ts +13 -0
- package/dist/src/pg/analyze.js +479 -0
- package/dist/src/pg/extensions.d.ts +2 -0
- package/dist/src/pg/extensions.js +15 -0
- package/dist/src/pg/narrow.d.ts +3 -0
- package/dist/src/pg/narrow.js +146 -0
- package/dist/src/pg/oids.d.ts +10 -0
- package/dist/src/pg/oids.js +137 -0
- package/dist/src/pg/param-map.d.ts +12 -0
- package/dist/src/pg/param-map.js +180 -0
- package/dist/src/pg/schema.d.ts +61 -0
- package/dist/src/pg/schema.js +225 -0
- package/dist/src/pg/wire.d.ts +134 -0
- package/dist/src/pg/wire.js +705 -0
- package/dist/src/postgres-runtime.d.ts +11 -0
- package/dist/src/postgres-runtime.js +150 -0
- package/dist/src/runtime.d.ts +69 -0
- package/dist/src/runtime.js +446 -0
- package/dist/src/scan/scanner.d.ts +12 -0
- package/dist/src/scan/scanner.js +283 -0
- package/dist/src/schema-snapshot.d.ts +104 -0
- package/dist/src/schema-snapshot.js +473 -0
- package/dist/src/typed.d.ts +25 -0
- package/dist/src/typed.js +1 -0
- package/package.json +78 -0
|
@@ -0,0 +1,705 @@
|
|
|
1
|
+
import { createHash, createHmac, pbkdf2Sync, randomBytes } from "node:crypto";
|
|
2
|
+
import { connect as netConnect } from "node:net";
|
|
3
|
+
import { connect as tlsConnect } from "node:tls";
|
|
4
|
+
const textEncoder = new TextEncoder();
|
|
5
|
+
export const SSL_MODES = ["disable", "prefer", "require", "verify-ca", "verify-full"];
|
|
6
|
+
export function parseDatabaseUrl(url) {
|
|
7
|
+
const u = new URL(url);
|
|
8
|
+
if (u.protocol !== "postgres:" && u.protocol !== "postgresql:") {
|
|
9
|
+
throw new Error(`unsupported scheme: ${u.protocol}`);
|
|
10
|
+
}
|
|
11
|
+
const params = u.searchParams;
|
|
12
|
+
const sslmodeRaw = params.get("sslmode") ?? undefined;
|
|
13
|
+
if (sslmodeRaw !== undefined && !SSL_MODES.includes(sslmodeRaw)) {
|
|
14
|
+
throw new Error(`unsupported sslmode: ${sslmodeRaw}`);
|
|
15
|
+
}
|
|
16
|
+
const cfg = {
|
|
17
|
+
host: u.hostname || "localhost",
|
|
18
|
+
port: u.port ? Number(u.port) : 5432,
|
|
19
|
+
user: decodeURIComponent(u.username || "postgres"),
|
|
20
|
+
password: decodeURIComponent(u.password || ""),
|
|
21
|
+
database: u.pathname.replace(/^\//, "") || decodeURIComponent(u.username || "postgres"),
|
|
22
|
+
};
|
|
23
|
+
if (sslmodeRaw !== undefined)
|
|
24
|
+
cfg.sslmode = sslmodeRaw;
|
|
25
|
+
const appName = params.get("application_name");
|
|
26
|
+
if (appName)
|
|
27
|
+
cfg.applicationName = appName;
|
|
28
|
+
const ct = params.get("connect_timeout");
|
|
29
|
+
if (ct) {
|
|
30
|
+
const n = Number(ct);
|
|
31
|
+
if (Number.isFinite(n) && n > 0)
|
|
32
|
+
cfg.connectTimeoutMs = n * 1000;
|
|
33
|
+
}
|
|
34
|
+
return cfg;
|
|
35
|
+
}
|
|
36
|
+
export class MessageReader {
|
|
37
|
+
chunks = [];
|
|
38
|
+
size = 0;
|
|
39
|
+
offset = 0;
|
|
40
|
+
push(chunk) {
|
|
41
|
+
this.chunks.push(chunk);
|
|
42
|
+
this.size += chunk.length;
|
|
43
|
+
return this.drain();
|
|
44
|
+
}
|
|
45
|
+
buffered() {
|
|
46
|
+
if (this.chunks.length === 1)
|
|
47
|
+
return this.chunks[0];
|
|
48
|
+
const out = new Uint8Array(this.size);
|
|
49
|
+
let off = 0;
|
|
50
|
+
for (const c of this.chunks) {
|
|
51
|
+
out.set(c, off);
|
|
52
|
+
off += c.length;
|
|
53
|
+
}
|
|
54
|
+
this.chunks = [out];
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
drain() {
|
|
58
|
+
const out = [];
|
|
59
|
+
while (true) {
|
|
60
|
+
const available = this.size - this.offset;
|
|
61
|
+
if (available < 5)
|
|
62
|
+
break;
|
|
63
|
+
const view = this.buffered();
|
|
64
|
+
const len = readInt32(view, this.offset + 1);
|
|
65
|
+
const total = 1 + len;
|
|
66
|
+
if (available < total)
|
|
67
|
+
break;
|
|
68
|
+
const tag = String.fromCharCode(view[this.offset]);
|
|
69
|
+
const payload = view.subarray(this.offset + 5, this.offset + total);
|
|
70
|
+
out.push(parseMessage(tag, copyOf(payload)));
|
|
71
|
+
this.offset += total;
|
|
72
|
+
}
|
|
73
|
+
if (this.offset > 0) {
|
|
74
|
+
const view = this.buffered();
|
|
75
|
+
const tail = view.subarray(this.offset);
|
|
76
|
+
this.chunks = tail.length > 0 ? [copyOf(tail)] : [];
|
|
77
|
+
this.size = tail.length;
|
|
78
|
+
this.offset = 0;
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function copyOf(view) {
|
|
84
|
+
const out = new Uint8Array(view.length);
|
|
85
|
+
out.set(view);
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
function parseMessage(tag, payload) {
|
|
89
|
+
switch (tag) {
|
|
90
|
+
case "R": {
|
|
91
|
+
const code = readInt32(payload, 0);
|
|
92
|
+
return { type: "R", code, payload: payload.subarray(4) };
|
|
93
|
+
}
|
|
94
|
+
case "S": {
|
|
95
|
+
const [name, rest] = readCString(payload, 0);
|
|
96
|
+
const [value] = readCString(payload, rest);
|
|
97
|
+
return { type: "S", name, value };
|
|
98
|
+
}
|
|
99
|
+
case "K":
|
|
100
|
+
return { type: "K", pid: readInt32(payload, 0), secret: readInt32(payload, 4) };
|
|
101
|
+
case "Z":
|
|
102
|
+
return { type: "Z", status: String.fromCharCode(payload[0]) };
|
|
103
|
+
case "1":
|
|
104
|
+
return { type: "1" };
|
|
105
|
+
case "2":
|
|
106
|
+
return { type: "2" };
|
|
107
|
+
case "3":
|
|
108
|
+
return { type: "3" };
|
|
109
|
+
case "n":
|
|
110
|
+
return { type: "n" };
|
|
111
|
+
case "t": {
|
|
112
|
+
const n = readInt16(payload, 0);
|
|
113
|
+
const oids = [];
|
|
114
|
+
for (let i = 0; i < n; i++)
|
|
115
|
+
oids.push(readInt32(payload, 2 + i * 4));
|
|
116
|
+
return { type: "t", oids };
|
|
117
|
+
}
|
|
118
|
+
case "T": {
|
|
119
|
+
const n = readInt16(payload, 0);
|
|
120
|
+
let off = 2;
|
|
121
|
+
const fields = [];
|
|
122
|
+
for (let i = 0; i < n; i++) {
|
|
123
|
+
const [name, next] = readCString(payload, off);
|
|
124
|
+
off = next;
|
|
125
|
+
const tableOid = readInt32(payload, off);
|
|
126
|
+
off += 4;
|
|
127
|
+
const columnAttr = readInt16(payload, off);
|
|
128
|
+
off += 2;
|
|
129
|
+
const typeOid = readInt32(payload, off);
|
|
130
|
+
off += 4;
|
|
131
|
+
const typeSize = readInt16(payload, off);
|
|
132
|
+
off += 2;
|
|
133
|
+
const typeModifier = readInt32(payload, off);
|
|
134
|
+
off += 4;
|
|
135
|
+
const format = readInt16(payload, off);
|
|
136
|
+
off += 2;
|
|
137
|
+
fields.push({ name, tableOid, columnAttr, typeOid, typeSize, typeModifier, format });
|
|
138
|
+
}
|
|
139
|
+
return { type: "T", fields };
|
|
140
|
+
}
|
|
141
|
+
case "E":
|
|
142
|
+
case "N": {
|
|
143
|
+
const fields = {};
|
|
144
|
+
let off = 0;
|
|
145
|
+
while (off < payload.length && payload[off] !== 0) {
|
|
146
|
+
const code = String.fromCharCode(payload[off]);
|
|
147
|
+
off += 1;
|
|
148
|
+
const [val, next] = readCString(payload, off);
|
|
149
|
+
fields[code] = val;
|
|
150
|
+
off = next;
|
|
151
|
+
}
|
|
152
|
+
return { type: tag, fields };
|
|
153
|
+
}
|
|
154
|
+
case "C": {
|
|
155
|
+
const [tagStr] = readCString(payload, 0);
|
|
156
|
+
return { type: "C", tag: tagStr };
|
|
157
|
+
}
|
|
158
|
+
case "D": {
|
|
159
|
+
const n = readInt16(payload, 0);
|
|
160
|
+
let off = 2;
|
|
161
|
+
const cols = [];
|
|
162
|
+
for (let i = 0; i < n; i++) {
|
|
163
|
+
const len = readInt32(payload, off);
|
|
164
|
+
off += 4;
|
|
165
|
+
if (len === -1)
|
|
166
|
+
cols.push(null);
|
|
167
|
+
else {
|
|
168
|
+
cols.push(payload.subarray(off, off + len));
|
|
169
|
+
off += len;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return { type: "D", columns: cols };
|
|
173
|
+
}
|
|
174
|
+
default:
|
|
175
|
+
return { type: "other", tag, payload };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function readInt16(b, o) {
|
|
179
|
+
return (b[o] << 8) | b[o + 1];
|
|
180
|
+
}
|
|
181
|
+
function readInt32(b, o) {
|
|
182
|
+
return ((b[o] << 24) | (b[o + 1] << 16) | (b[o + 2] << 8) | b[o + 3]) | 0;
|
|
183
|
+
}
|
|
184
|
+
function readCString(b, off) {
|
|
185
|
+
let end = off;
|
|
186
|
+
while (end < b.length && b[end] !== 0)
|
|
187
|
+
end++;
|
|
188
|
+
const s = new TextDecoder("utf-8").decode(b.subarray(off, end));
|
|
189
|
+
return [s, end + 1];
|
|
190
|
+
}
|
|
191
|
+
function writeInt32(n) {
|
|
192
|
+
const b = new Uint8Array(4);
|
|
193
|
+
b[0] = (n >>> 24) & 0xff;
|
|
194
|
+
b[1] = (n >>> 16) & 0xff;
|
|
195
|
+
b[2] = (n >>> 8) & 0xff;
|
|
196
|
+
b[3] = n & 0xff;
|
|
197
|
+
return b;
|
|
198
|
+
}
|
|
199
|
+
function writeInt16(n) {
|
|
200
|
+
const b = new Uint8Array(2);
|
|
201
|
+
b[0] = (n >>> 8) & 0xff;
|
|
202
|
+
b[1] = n & 0xff;
|
|
203
|
+
return b;
|
|
204
|
+
}
|
|
205
|
+
function cstr(s) {
|
|
206
|
+
const enc = textEncoder.encode(s);
|
|
207
|
+
const out = new Uint8Array(enc.length + 1);
|
|
208
|
+
out.set(enc);
|
|
209
|
+
return out;
|
|
210
|
+
}
|
|
211
|
+
function concat(parts) {
|
|
212
|
+
const len = parts.reduce((a, p) => a + p.length, 0);
|
|
213
|
+
const out = new Uint8Array(len);
|
|
214
|
+
let off = 0;
|
|
215
|
+
for (const p of parts) {
|
|
216
|
+
out.set(p, off);
|
|
217
|
+
off += p.length;
|
|
218
|
+
}
|
|
219
|
+
return out;
|
|
220
|
+
}
|
|
221
|
+
function frame(tag, body) {
|
|
222
|
+
const lenBytes = writeInt32(body.length + 4);
|
|
223
|
+
if (tag === null)
|
|
224
|
+
return concat([lenBytes, body]);
|
|
225
|
+
const tb = new Uint8Array(1);
|
|
226
|
+
tb[0] = tag.charCodeAt(0);
|
|
227
|
+
return concat([tb, lenBytes, body]);
|
|
228
|
+
}
|
|
229
|
+
function isTlsRequired(mode) {
|
|
230
|
+
return mode === "require" || mode === "verify-ca" || mode === "verify-full";
|
|
231
|
+
}
|
|
232
|
+
async function openPlainSocket(host, port, timeoutMs) {
|
|
233
|
+
return new Promise((resolve, reject) => {
|
|
234
|
+
const sock = netConnect({ host, port });
|
|
235
|
+
const t = setTimeout(() => {
|
|
236
|
+
sock.destroy();
|
|
237
|
+
reject(new Error(`sqlx-js: TCP connect timeout to ${host}:${port} after ${timeoutMs}ms`));
|
|
238
|
+
}, timeoutMs);
|
|
239
|
+
sock.once("connect", () => {
|
|
240
|
+
clearTimeout(t);
|
|
241
|
+
sock.setNoDelay(true);
|
|
242
|
+
resolve(sock);
|
|
243
|
+
});
|
|
244
|
+
sock.once("error", (err) => {
|
|
245
|
+
clearTimeout(t);
|
|
246
|
+
reject(err);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
async function performSslHandshake(sock, cfg, mode) {
|
|
251
|
+
const reply = await new Promise((resolve, reject) => {
|
|
252
|
+
const onData = (chunk) => {
|
|
253
|
+
sock.removeListener("error", onError);
|
|
254
|
+
if (chunk.length === 0) {
|
|
255
|
+
reject(new Error("ssl: empty handshake reply"));
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (chunk.length > 1) {
|
|
259
|
+
sock.unshift(chunk.subarray(1));
|
|
260
|
+
}
|
|
261
|
+
resolve(chunk[0]);
|
|
262
|
+
};
|
|
263
|
+
const onError = (err) => {
|
|
264
|
+
sock.removeListener("data", onData);
|
|
265
|
+
reject(err);
|
|
266
|
+
};
|
|
267
|
+
sock.once("data", onData);
|
|
268
|
+
sock.once("error", onError);
|
|
269
|
+
sock.write(frame(null, writeInt32(80877103)));
|
|
270
|
+
});
|
|
271
|
+
if (reply === "S".charCodeAt(0)) {
|
|
272
|
+
const tlsSock = await new Promise((resolve, reject) => {
|
|
273
|
+
const t = tlsConnect({
|
|
274
|
+
socket: sock,
|
|
275
|
+
servername: cfg.host,
|
|
276
|
+
rejectUnauthorized: mode === "verify-full" || mode === "verify-ca",
|
|
277
|
+
checkServerIdentity: mode === "verify-full" ? undefined : () => undefined,
|
|
278
|
+
});
|
|
279
|
+
t.once("secureConnect", () => resolve(t));
|
|
280
|
+
t.once("error", (err) => {
|
|
281
|
+
sock.destroy();
|
|
282
|
+
reject(err);
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
return { sock: tlsSock, tls: true };
|
|
286
|
+
}
|
|
287
|
+
if (reply === "N".charCodeAt(0)) {
|
|
288
|
+
if (isTlsRequired(mode)) {
|
|
289
|
+
sock.destroy();
|
|
290
|
+
throw new Error(`sqlx-js: server rejected SSL but sslmode=${mode} requires it`);
|
|
291
|
+
}
|
|
292
|
+
return { sock, tls: false };
|
|
293
|
+
}
|
|
294
|
+
sock.destroy();
|
|
295
|
+
throw new Error(`sqlx-js: unexpected SSL handshake reply byte 0x${reply.toString(16)}`);
|
|
296
|
+
}
|
|
297
|
+
export class PgClient {
|
|
298
|
+
cfg;
|
|
299
|
+
sock;
|
|
300
|
+
reader = new MessageReader();
|
|
301
|
+
queue = [];
|
|
302
|
+
waiters = [];
|
|
303
|
+
closed = false;
|
|
304
|
+
closeReason = null;
|
|
305
|
+
tlsEnabled = false;
|
|
306
|
+
constructor(cfg) {
|
|
307
|
+
this.cfg = cfg;
|
|
308
|
+
}
|
|
309
|
+
get usingTls() { return this.tlsEnabled; }
|
|
310
|
+
async connect() {
|
|
311
|
+
const mode = this.cfg.sslmode ?? "prefer";
|
|
312
|
+
const timeoutMs = this.cfg.connectTimeoutMs ?? 15000;
|
|
313
|
+
const plain = await openPlainSocket(this.cfg.host, this.cfg.port, timeoutMs);
|
|
314
|
+
let socket = plain;
|
|
315
|
+
if (mode !== "disable") {
|
|
316
|
+
const result = await performSslHandshake(plain, this.cfg, mode);
|
|
317
|
+
socket = result.sock;
|
|
318
|
+
this.tlsEnabled = result.tls;
|
|
319
|
+
}
|
|
320
|
+
this.sock = socket;
|
|
321
|
+
this.attachHandlers();
|
|
322
|
+
await this.startup();
|
|
323
|
+
await this.authenticate();
|
|
324
|
+
await this.awaitReady();
|
|
325
|
+
}
|
|
326
|
+
attachHandlers() {
|
|
327
|
+
const onData = (chunk) => {
|
|
328
|
+
for (const m of this.reader.push(chunk))
|
|
329
|
+
this.deliver(m);
|
|
330
|
+
};
|
|
331
|
+
const onClose = () => {
|
|
332
|
+
this.closed = true;
|
|
333
|
+
if (!this.closeReason)
|
|
334
|
+
this.closeReason = new Error("connection closed");
|
|
335
|
+
this.flushWaiters();
|
|
336
|
+
};
|
|
337
|
+
const onError = (err) => {
|
|
338
|
+
this.closed = true;
|
|
339
|
+
if (!this.closeReason)
|
|
340
|
+
this.closeReason = err;
|
|
341
|
+
this.flushWaiters();
|
|
342
|
+
};
|
|
343
|
+
this.sock.on("data", onData);
|
|
344
|
+
this.sock.on("close", onClose);
|
|
345
|
+
this.sock.on("error", onError);
|
|
346
|
+
}
|
|
347
|
+
deliver(msg) {
|
|
348
|
+
const w = this.waiters.shift();
|
|
349
|
+
if (w)
|
|
350
|
+
w.resolve(msg);
|
|
351
|
+
else
|
|
352
|
+
this.queue.push(msg);
|
|
353
|
+
}
|
|
354
|
+
flushWaiters() {
|
|
355
|
+
const reason = this.closeReason ?? new Error("connection closed");
|
|
356
|
+
const err = new ConnectionLostError(reason);
|
|
357
|
+
while (this.waiters.length) {
|
|
358
|
+
const w = this.waiters.shift();
|
|
359
|
+
w.reject(err);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
next() {
|
|
363
|
+
if (this.queue.length)
|
|
364
|
+
return Promise.resolve(this.queue.shift());
|
|
365
|
+
if (this.closed) {
|
|
366
|
+
const reason = this.closeReason ?? new Error("connection closed");
|
|
367
|
+
return Promise.reject(new ConnectionLostError(reason));
|
|
368
|
+
}
|
|
369
|
+
return new Promise((resolve, reject) => this.waiters.push({ resolve, reject }));
|
|
370
|
+
}
|
|
371
|
+
write(buf) {
|
|
372
|
+
this.sock.write(buf);
|
|
373
|
+
}
|
|
374
|
+
async startup() {
|
|
375
|
+
const pairs = [
|
|
376
|
+
cstr("user"), cstr(this.cfg.user),
|
|
377
|
+
cstr("database"), cstr(this.cfg.database),
|
|
378
|
+
cstr("client_encoding"), cstr("UTF8"),
|
|
379
|
+
];
|
|
380
|
+
if (this.cfg.applicationName) {
|
|
381
|
+
pairs.push(cstr("application_name"), cstr(this.cfg.applicationName));
|
|
382
|
+
}
|
|
383
|
+
pairs.push(new Uint8Array([0]));
|
|
384
|
+
const body = concat([writeInt32(196608), concat(pairs)]);
|
|
385
|
+
this.write(frame(null, body));
|
|
386
|
+
}
|
|
387
|
+
async authenticate() {
|
|
388
|
+
while (true) {
|
|
389
|
+
const m = await this.next();
|
|
390
|
+
if (m.type !== "R")
|
|
391
|
+
throw new Error(`expected R, got ${m.type}: ${stringifyMessage(m)}`);
|
|
392
|
+
if (m.code === 0)
|
|
393
|
+
return;
|
|
394
|
+
if (m.code === 3) {
|
|
395
|
+
const body = cstr(this.cfg.password);
|
|
396
|
+
this.write(frame("p", body));
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
if (m.code === 5) {
|
|
400
|
+
const salt = m.payload.subarray(0, 4);
|
|
401
|
+
const md5 = (data) => createHash("md5").update(typeof data === "string" ? Buffer.from(data) : data).digest("hex");
|
|
402
|
+
const inner = md5(this.cfg.password + this.cfg.user);
|
|
403
|
+
const combined = Buffer.concat([Buffer.from(inner, "utf8"), Buffer.from(salt)]);
|
|
404
|
+
const outer = "md5" + md5(combined);
|
|
405
|
+
this.write(frame("p", cstr(outer)));
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
if (m.code === 10) {
|
|
409
|
+
await this.scramAuth(m.payload);
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
throw new Error(`unsupported auth code ${m.code}`);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
async scramAuth(initialPayload) {
|
|
416
|
+
const mechs = [];
|
|
417
|
+
let off = 0;
|
|
418
|
+
while (off < initialPayload.length) {
|
|
419
|
+
const [name, next] = readCString(initialPayload, off);
|
|
420
|
+
if (!name)
|
|
421
|
+
break;
|
|
422
|
+
mechs.push(name);
|
|
423
|
+
off = next;
|
|
424
|
+
}
|
|
425
|
+
if (!mechs.includes("SCRAM-SHA-256")) {
|
|
426
|
+
throw new Error(`server offered ${mechs.join(",")}; no SCRAM-SHA-256`);
|
|
427
|
+
}
|
|
428
|
+
const clientNonce = randomBytes(18).toString("base64");
|
|
429
|
+
const clientFirstBare = `n=,r=${clientNonce}`;
|
|
430
|
+
const clientFirst = `n,,${clientFirstBare}`;
|
|
431
|
+
const initialBody = concat([
|
|
432
|
+
cstr("SCRAM-SHA-256"),
|
|
433
|
+
writeInt32(clientFirst.length),
|
|
434
|
+
textEncoder.encode(clientFirst),
|
|
435
|
+
]);
|
|
436
|
+
this.write(frame("p", initialBody));
|
|
437
|
+
const m1 = await this.next();
|
|
438
|
+
if (m1.type !== "R" || m1.code !== 11)
|
|
439
|
+
throw new Error(`SCRAM: expected R/11, got ${stringifyMessage(m1)}`);
|
|
440
|
+
const serverFirst = new TextDecoder().decode(m1.payload);
|
|
441
|
+
const sf = parseScramKv(serverFirst);
|
|
442
|
+
const combinedNonce = scramField(sf, "r");
|
|
443
|
+
if (!combinedNonce.startsWith(clientNonce))
|
|
444
|
+
throw new Error("SCRAM: server nonce mismatch");
|
|
445
|
+
const salt = Buffer.from(scramField(sf, "s"), "base64");
|
|
446
|
+
const iterations = parseInt(scramField(sf, "i"), 10);
|
|
447
|
+
const clientFinalNoProof = `c=biws,r=${combinedNonce}`;
|
|
448
|
+
const authMessage = `${clientFirstBare},${serverFirst},${clientFinalNoProof}`;
|
|
449
|
+
const { clientProofB64, serverSignatureB64 } = computeScramProof(this.cfg.password, salt, iterations, authMessage);
|
|
450
|
+
const clientFinal = `${clientFinalNoProof},p=${clientProofB64}`;
|
|
451
|
+
this.write(frame("p", textEncoder.encode(clientFinal)));
|
|
452
|
+
const m2 = await this.next();
|
|
453
|
+
if (m2.type !== "R" || m2.code !== 12)
|
|
454
|
+
throw new Error(`SCRAM: expected R/12, got ${stringifyMessage(m2)}`);
|
|
455
|
+
const serverFinal = new TextDecoder().decode(m2.payload);
|
|
456
|
+
const sfKv = parseScramKv(serverFinal);
|
|
457
|
+
if (sfKv.e)
|
|
458
|
+
throw new Error(`SCRAM server error: ${sfKv.e}`);
|
|
459
|
+
if (sfKv.v !== serverSignatureB64)
|
|
460
|
+
throw new Error("SCRAM: server signature mismatch");
|
|
461
|
+
}
|
|
462
|
+
async awaitReady() {
|
|
463
|
+
while (true) {
|
|
464
|
+
const m = await this.next();
|
|
465
|
+
if (m.type === "Z")
|
|
466
|
+
return;
|
|
467
|
+
if (m.type === "E")
|
|
468
|
+
throw pgError(m.fields);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
async describe(sql) {
|
|
472
|
+
const stmtName = "";
|
|
473
|
+
const parseBody = concat([
|
|
474
|
+
cstr(stmtName),
|
|
475
|
+
cstr(sql),
|
|
476
|
+
writeInt16(0),
|
|
477
|
+
]);
|
|
478
|
+
this.write(frame("P", parseBody));
|
|
479
|
+
const describeBody = concat([
|
|
480
|
+
new Uint8Array([0x53]),
|
|
481
|
+
cstr(stmtName),
|
|
482
|
+
]);
|
|
483
|
+
this.write(frame("D", describeBody));
|
|
484
|
+
this.write(frame("S", new Uint8Array(0)));
|
|
485
|
+
let paramOids = [];
|
|
486
|
+
let fields = [];
|
|
487
|
+
let sawNoData = false;
|
|
488
|
+
let sawRowDesc = false;
|
|
489
|
+
let err = null;
|
|
490
|
+
while (true) {
|
|
491
|
+
const m = await this.next();
|
|
492
|
+
if (m.type === "1")
|
|
493
|
+
continue;
|
|
494
|
+
if (m.type === "t") {
|
|
495
|
+
paramOids = m.oids;
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
if (m.type === "T") {
|
|
499
|
+
fields = m.fields;
|
|
500
|
+
sawRowDesc = true;
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
if (m.type === "n") {
|
|
504
|
+
sawNoData = true;
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
if (m.type === "Z")
|
|
508
|
+
break;
|
|
509
|
+
if (m.type === "E") {
|
|
510
|
+
if (!err)
|
|
511
|
+
err = pgError(m.fields);
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
if (err)
|
|
516
|
+
throw err;
|
|
517
|
+
if (!sawRowDesc && !sawNoData)
|
|
518
|
+
throw new Error("describe: neither RowDescription nor NoData");
|
|
519
|
+
return { paramOids, fields };
|
|
520
|
+
}
|
|
521
|
+
async execParamsText(sql, params) {
|
|
522
|
+
const stmtName = "";
|
|
523
|
+
const portal = "";
|
|
524
|
+
const parseBody = concat([cstr(stmtName), cstr(sql), writeInt16(0)]);
|
|
525
|
+
this.write(frame("P", parseBody));
|
|
526
|
+
const bindParts = [
|
|
527
|
+
cstr(portal),
|
|
528
|
+
cstr(stmtName),
|
|
529
|
+
writeInt16(0),
|
|
530
|
+
writeInt16(params.length),
|
|
531
|
+
];
|
|
532
|
+
for (const p of params) {
|
|
533
|
+
if (p === null) {
|
|
534
|
+
bindParts.push(writeInt32(-1));
|
|
535
|
+
}
|
|
536
|
+
else {
|
|
537
|
+
const bytes = textEncoder.encode(p);
|
|
538
|
+
bindParts.push(writeInt32(bytes.length));
|
|
539
|
+
bindParts.push(bytes);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
bindParts.push(writeInt16(0));
|
|
543
|
+
this.write(frame("B", concat(bindParts)));
|
|
544
|
+
const describeBody = concat([new Uint8Array([0x50]), cstr(portal)]);
|
|
545
|
+
this.write(frame("D", describeBody));
|
|
546
|
+
const executeBody = concat([cstr(portal), writeInt32(0)]);
|
|
547
|
+
this.write(frame("E", executeBody));
|
|
548
|
+
this.write(frame("S", new Uint8Array(0)));
|
|
549
|
+
const rows = [];
|
|
550
|
+
let fields = [];
|
|
551
|
+
let tag = "";
|
|
552
|
+
let err = null;
|
|
553
|
+
while (true) {
|
|
554
|
+
const m = await this.next();
|
|
555
|
+
if (m.type === "1" || m.type === "2")
|
|
556
|
+
continue;
|
|
557
|
+
if (m.type === "T") {
|
|
558
|
+
fields = m.fields;
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
if (m.type === "n")
|
|
562
|
+
continue;
|
|
563
|
+
if (m.type === "D") {
|
|
564
|
+
rows.push(m.columns);
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
if (m.type === "C") {
|
|
568
|
+
tag = m.tag;
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
if (m.type === "Z")
|
|
572
|
+
break;
|
|
573
|
+
if (m.type === "E") {
|
|
574
|
+
if (!err)
|
|
575
|
+
err = pgError(m.fields);
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
if (err)
|
|
580
|
+
throw err;
|
|
581
|
+
return { rows, fields, tag };
|
|
582
|
+
}
|
|
583
|
+
async simpleQueryAll(sql) {
|
|
584
|
+
this.write(frame("Q", cstr(sql)));
|
|
585
|
+
const allRows = [];
|
|
586
|
+
let lastFields = [];
|
|
587
|
+
const tags = [];
|
|
588
|
+
let err = null;
|
|
589
|
+
while (true) {
|
|
590
|
+
const m = await this.next();
|
|
591
|
+
if (m.type === "T")
|
|
592
|
+
lastFields = m.fields;
|
|
593
|
+
else if (m.type === "D")
|
|
594
|
+
allRows.push(m.columns);
|
|
595
|
+
else if (m.type === "C")
|
|
596
|
+
tags.push(m.tag);
|
|
597
|
+
else if (m.type === "Z")
|
|
598
|
+
break;
|
|
599
|
+
else if (m.type === "E") {
|
|
600
|
+
if (!err)
|
|
601
|
+
err = pgError(m.fields);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
if (err)
|
|
605
|
+
throw err;
|
|
606
|
+
return { rows: allRows, fields: lastFields, tags };
|
|
607
|
+
}
|
|
608
|
+
async simpleQuery(sql) {
|
|
609
|
+
this.write(frame("Q", cstr(sql)));
|
|
610
|
+
const rows = [];
|
|
611
|
+
let fields = [];
|
|
612
|
+
let tag = "";
|
|
613
|
+
let err = null;
|
|
614
|
+
while (true) {
|
|
615
|
+
const m = await this.next();
|
|
616
|
+
if (m.type === "T")
|
|
617
|
+
fields = m.fields;
|
|
618
|
+
else if (m.type === "D")
|
|
619
|
+
rows.push(m.columns);
|
|
620
|
+
else if (m.type === "C")
|
|
621
|
+
tag = m.tag;
|
|
622
|
+
else if (m.type === "Z")
|
|
623
|
+
break;
|
|
624
|
+
else if (m.type === "E") {
|
|
625
|
+
if (!err)
|
|
626
|
+
err = pgError(m.fields);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
if (err)
|
|
630
|
+
throw err;
|
|
631
|
+
return { rows, fields, tag };
|
|
632
|
+
}
|
|
633
|
+
async end() {
|
|
634
|
+
try {
|
|
635
|
+
this.write(frame("X", new Uint8Array(0)));
|
|
636
|
+
}
|
|
637
|
+
catch { }
|
|
638
|
+
try {
|
|
639
|
+
this.sock.end();
|
|
640
|
+
}
|
|
641
|
+
catch { }
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
export function computeScramProof(password, salt, iterations, authMessage) {
|
|
645
|
+
const saltedPassword = pbkdf2Sync(password, Buffer.from(salt), iterations, 32, "sha256");
|
|
646
|
+
const clientKey = createHmac("sha256", saltedPassword).update("Client Key").digest();
|
|
647
|
+
const storedKey = createHash("sha256").update(clientKey).digest();
|
|
648
|
+
const clientSignature = createHmac("sha256", storedKey).update(authMessage).digest();
|
|
649
|
+
const clientProof = Buffer.alloc(clientKey.length);
|
|
650
|
+
for (let i = 0; i < clientKey.length; i++)
|
|
651
|
+
clientProof[i] = clientKey[i] ^ clientSignature[i];
|
|
652
|
+
const serverKey = createHmac("sha256", saltedPassword).update("Server Key").digest();
|
|
653
|
+
const serverSignature = createHmac("sha256", serverKey).update(authMessage).digest();
|
|
654
|
+
return {
|
|
655
|
+
saltedPassword,
|
|
656
|
+
clientProofB64: clientProof.toString("base64"),
|
|
657
|
+
serverSignatureB64: serverSignature.toString("base64"),
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
function parseScramKv(s) {
|
|
661
|
+
const out = {};
|
|
662
|
+
for (const part of s.split(",")) {
|
|
663
|
+
const eq = part.indexOf("=");
|
|
664
|
+
if (eq < 0)
|
|
665
|
+
continue;
|
|
666
|
+
out[part.slice(0, eq)] = part.slice(eq + 1);
|
|
667
|
+
}
|
|
668
|
+
return out;
|
|
669
|
+
}
|
|
670
|
+
function scramField(kv, key) {
|
|
671
|
+
const v = kv[key];
|
|
672
|
+
if (v === undefined)
|
|
673
|
+
throw new Error(`SCRAM: missing field "${key}"`);
|
|
674
|
+
return v;
|
|
675
|
+
}
|
|
676
|
+
function stringifyMessage(m) {
|
|
677
|
+
return JSON.stringify(m, (_k, v) => v instanceof Uint8Array ? `<${v.length}B>` : v);
|
|
678
|
+
}
|
|
679
|
+
export class PgError extends Error {
|
|
680
|
+
fields;
|
|
681
|
+
constructor(fields) {
|
|
682
|
+
super(fields.M ?? "postgres error");
|
|
683
|
+
this.fields = fields;
|
|
684
|
+
this.name = "PgError";
|
|
685
|
+
}
|
|
686
|
+
get code() { return this.fields.C; }
|
|
687
|
+
get position() { return this.fields.P ? Number(this.fields.P) : undefined; }
|
|
688
|
+
get hint() { return this.fields.H; }
|
|
689
|
+
get detail() { return this.fields.D; }
|
|
690
|
+
get severity() { return this.fields.S; }
|
|
691
|
+
}
|
|
692
|
+
export class ConnectionLostError extends Error {
|
|
693
|
+
cause;
|
|
694
|
+
constructor(cause) {
|
|
695
|
+
super(`sqlx-js: connection lost: ${cause.message}`);
|
|
696
|
+
this.cause = cause;
|
|
697
|
+
this.name = "ConnectionLostError";
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
function pgError(fields) {
|
|
701
|
+
return new PgError(fields);
|
|
702
|
+
}
|
|
703
|
+
export function decodeText(b) {
|
|
704
|
+
return b === null ? null : new TextDecoder().decode(b);
|
|
705
|
+
}
|