tinbase 0.8.1 → 0.10.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/assets/logo.svg +9 -13
- package/dist/admin/ui.d.ts +1 -1
- package/dist/admin/ui.js +1 -1
- package/dist/auth/handler.d.ts +25 -0
- package/dist/auth/handler.js +127 -8
- package/dist/auth/handler.js.map +1 -1
- package/dist/auth/oauth.d.ts +1 -0
- package/dist/auth/oauth.js +28 -9
- package/dist/auth/oauth.js.map +1 -1
- package/dist/cli.js +23 -9
- package/dist/cli.js.map +1 -1
- package/dist/cron/service.js +2 -1
- package/dist/cron/service.js.map +1 -1
- package/dist/db/bootstrap.d.ts +2 -2
- package/dist/db/bootstrap.js +19 -0
- package/dist/db/bootstrap.js.map +1 -1
- package/dist/db/database.d.ts +3 -1
- package/dist/db/database.js +6 -1
- package/dist/db/database.js.map +1 -1
- package/dist/db/emulated.d.ts +3 -3
- package/dist/db/emulated.js +46 -13
- package/dist/db/emulated.js.map +1 -1
- package/dist/functions/deno-shim.d.ts +3 -2
- package/dist/functions/deno-shim.js +12 -4
- package/dist/functions/deno-shim.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +32 -6
- package/dist/index.js.map +1 -1
- package/dist/jwt.js +5 -0
- package/dist/jwt.js.map +1 -1
- package/dist/net/service.d.ts +9 -0
- package/dist/net/service.js +97 -10
- package/dist/net/service.js.map +1 -1
- package/dist/node/native/database-url.d.ts +25 -0
- package/dist/node/native/database-url.js +38 -0
- package/dist/node/native/database-url.js.map +1 -0
- package/dist/node/native/engine.js +5 -78
- package/dist/node/native/engine.js.map +1 -1
- package/dist/node/native/wire-engine.d.ts +16 -0
- package/dist/node/native/wire-engine.js +83 -0
- package/dist/node/native/wire-engine.js.map +1 -0
- package/dist/node/native/wire.d.ts +10 -7
- package/dist/node/native/wire.js +98 -5
- package/dist/node/native/wire.js.map +1 -1
- package/dist/node/ws.d.ts +9 -0
- package/dist/node/ws.js +8 -2
- package/dist/node/ws.js.map +1 -1
- package/dist/realtime/engine.js +19 -1
- package/dist/realtime/engine.js.map +1 -1
- package/dist/rest/build.d.ts +7 -0
- package/dist/rest/build.js +51 -5
- package/dist/rest/build.js.map +1 -1
- package/dist/rest/handler.js +77 -5
- package/dist/rest/handler.js.map +1 -1
- package/dist/rest/parse.d.ts +2 -0
- package/dist/rest/parse.js +22 -1
- package/dist/rest/parse.js.map +1 -1
- package/dist/retention/service.d.ts +37 -0
- package/dist/retention/service.js +66 -0
- package/dist/retention/service.js.map +1 -0
- package/dist/storage/handler.d.ts +10 -0
- package/dist/storage/handler.js +230 -28
- package/dist/storage/handler.js.map +1 -1
- package/dist/types.d.ts +21 -0
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
package/dist/net/service.js
CHANGED
|
@@ -1,3 +1,54 @@
|
|
|
1
|
+
/** Cap on the response body we buffer into net._http_response (bytes). */
|
|
2
|
+
const MAX_RESPONSE_BYTES = 10 * 1024 * 1024;
|
|
3
|
+
/**
|
|
4
|
+
* Reject requests to non-public destinations. This is a literal-host guard
|
|
5
|
+
* (loopback / private / link-local / cloud-metadata), which blocks the common
|
|
6
|
+
* SSRF vectors without an async DNS lookup. Returns an error string, or null
|
|
7
|
+
* when the URL is allowed.
|
|
8
|
+
*/
|
|
9
|
+
export function blockedNetTarget(rawUrl) {
|
|
10
|
+
let u;
|
|
11
|
+
try {
|
|
12
|
+
u = new URL(rawUrl);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return 'invalid url';
|
|
16
|
+
}
|
|
17
|
+
if (u.protocol !== 'http:' && u.protocol !== 'https:')
|
|
18
|
+
return `unsupported scheme: ${u.protocol}`;
|
|
19
|
+
const host = u.hostname.toLowerCase().replace(/^\[|\]$/g, '');
|
|
20
|
+
if (host === 'localhost' || host.endsWith('.localhost'))
|
|
21
|
+
return 'blocked host: localhost';
|
|
22
|
+
if (isPrivateIp(host))
|
|
23
|
+
return `blocked host: ${host}`;
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
function isPrivateIp(host) {
|
|
27
|
+
// IPv4
|
|
28
|
+
const m = host.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
29
|
+
if (m) {
|
|
30
|
+
const [a, b] = [Number(m[1]), Number(m[2])];
|
|
31
|
+
if (a === 0 || a === 10 || a === 127)
|
|
32
|
+
return true; // this-host, private, loopback
|
|
33
|
+
if (a === 169 && b === 254)
|
|
34
|
+
return true; // link-local (incl. 169.254.169.254 metadata)
|
|
35
|
+
if (a === 172 && b >= 16 && b <= 31)
|
|
36
|
+
return true; // private
|
|
37
|
+
if (a === 192 && b === 168)
|
|
38
|
+
return true; // private
|
|
39
|
+
if (a === 100 && b >= 64 && b <= 127)
|
|
40
|
+
return true; // carrier-grade NAT
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
// IPv6
|
|
44
|
+
if (host === '::' || host === '::1')
|
|
45
|
+
return true; // unspecified / loopback
|
|
46
|
+
if (host.startsWith('fe80') || host.startsWith('fc') || host.startsWith('fd'))
|
|
47
|
+
return true; // link-local / ULA
|
|
48
|
+
if (host.startsWith('::ffff:'))
|
|
49
|
+
return isPrivateIp(host.slice(7)); // IPv4-mapped
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
1
52
|
export class NetService {
|
|
2
53
|
db;
|
|
3
54
|
fetchImpl;
|
|
@@ -55,6 +106,13 @@ export class NetService {
|
|
|
55
106
|
let respHeaders = null;
|
|
56
107
|
let timedOut = false;
|
|
57
108
|
let errorMsg = null;
|
|
109
|
+
const blocked = blockedNetTarget(row.url);
|
|
110
|
+
if (blocked) {
|
|
111
|
+
clearTimeout(timer);
|
|
112
|
+
await this.record(row.id, null, null, null, content, false, blocked);
|
|
113
|
+
this.onDeliver?.({ id: row.id, method: row.method, url: row.url, timedOut: false, error: blocked });
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
58
116
|
try {
|
|
59
117
|
const hasBody = row.method !== 'GET' && row.method !== 'HEAD';
|
|
60
118
|
const res = await this.fetchImpl(row.url, {
|
|
@@ -66,7 +124,7 @@ export class NetService {
|
|
|
66
124
|
status = res.status;
|
|
67
125
|
contentType = res.headers.get('content-type');
|
|
68
126
|
respHeaders = Object.fromEntries(res.headers.entries());
|
|
69
|
-
content = await res
|
|
127
|
+
content = await readCapped(res, MAX_RESPONSE_BYTES);
|
|
70
128
|
}
|
|
71
129
|
catch (e) {
|
|
72
130
|
if (controller.signal.aborted)
|
|
@@ -76,15 +134,7 @@ export class NetService {
|
|
|
76
134
|
finally {
|
|
77
135
|
clearTimeout(timer);
|
|
78
136
|
}
|
|
79
|
-
|
|
80
|
-
try {
|
|
81
|
-
await this.db.query(`insert into net._http_response (id, status_code, content_type, headers, content, timed_out, error_msg)
|
|
82
|
-
values ($1, $2, $3, $4::jsonb, $5, $6, $7) on conflict (id) do nothing`, [row.id, status, contentType, respHeaders ? JSON.stringify(respHeaders) : null, content, timedOut, errorMsg]);
|
|
83
|
-
await this.db.query(`delete from net.http_request_queue where id = $1`, [row.id]);
|
|
84
|
-
}
|
|
85
|
-
catch {
|
|
86
|
-
// if recording fails, leave the row so the next tick retries
|
|
87
|
-
}
|
|
137
|
+
await this.record(row.id, status, contentType, respHeaders, content, timedOut, errorMsg);
|
|
88
138
|
this.onDeliver?.({
|
|
89
139
|
id: row.id,
|
|
90
140
|
method: row.method,
|
|
@@ -94,5 +144,42 @@ export class NetService {
|
|
|
94
144
|
error: errorMsg ?? undefined,
|
|
95
145
|
});
|
|
96
146
|
}
|
|
147
|
+
/** Record the response and remove the request from the queue (best-effort). */
|
|
148
|
+
async record(id, status, contentType, respHeaders, content, timedOut, errorMsg) {
|
|
149
|
+
try {
|
|
150
|
+
await this.db.query(`insert into net._http_response (id, status_code, content_type, headers, content, timed_out, error_msg)
|
|
151
|
+
values ($1, $2, $3, $4::jsonb, $5, $6, $7) on conflict (id) do nothing`, [id, status, contentType, respHeaders ? JSON.stringify(respHeaders) : null, content, timedOut, errorMsg]);
|
|
152
|
+
await this.db.query(`delete from net.http_request_queue where id = $1`, [id]);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
// if recording fails, leave the row so the next tick retries
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/** Read a response body as text, truncating at `maxBytes` to bound memory. */
|
|
160
|
+
async function readCapped(res, maxBytes) {
|
|
161
|
+
if (!res.body)
|
|
162
|
+
return await res.text();
|
|
163
|
+
const reader = res.body.getReader();
|
|
164
|
+
const chunks = [];
|
|
165
|
+
let total = 0;
|
|
166
|
+
while (total < maxBytes) {
|
|
167
|
+
const { done, value } = await reader.read();
|
|
168
|
+
if (done)
|
|
169
|
+
break;
|
|
170
|
+
chunks.push(value);
|
|
171
|
+
total += value.length;
|
|
172
|
+
}
|
|
173
|
+
await reader.cancel().catch(() => { });
|
|
174
|
+
let merged = new Uint8Array(Math.min(total, maxBytes));
|
|
175
|
+
let offset = 0;
|
|
176
|
+
for (const c of chunks) {
|
|
177
|
+
const take = Math.min(c.length, merged.length - offset);
|
|
178
|
+
merged.set(c.subarray(0, take), offset);
|
|
179
|
+
offset += take;
|
|
180
|
+
if (offset >= merged.length)
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
return new TextDecoder().decode(merged);
|
|
97
184
|
}
|
|
98
185
|
//# sourceMappingURL=service.js.map
|
package/dist/net/service.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.js","sourceRoot":"","sources":["../../src/net/service.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"service.js","sourceRoot":"","sources":["../../src/net/service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External-Postgres engine: point tinbase's REST/Auth/Storage at a Postgres you
|
|
3
|
+
* already run, via `--database-url` / `createBackend({ databaseUrl })`. A thin
|
|
4
|
+
* DbEngine over PgWireClient (TCP + cleartext/md5/SCRAM auth), reusing the same
|
|
5
|
+
* engine machinery as the embedded native engine.
|
|
6
|
+
*
|
|
7
|
+
* The target is treated as shared/pre-existing: bootstrap runs idempotently and
|
|
8
|
+
* never assumes exclusive ownership or an empty database. (TLS/sslmode, realtime
|
|
9
|
+
* CDC, RLS-role provisioning edge cases, and pooling are follow-ups.)
|
|
10
|
+
*/
|
|
11
|
+
import type { DbEngine } from '../../db/engine.js';
|
|
12
|
+
export interface ParsedConnectionString {
|
|
13
|
+
host: string;
|
|
14
|
+
port: number;
|
|
15
|
+
user: string;
|
|
16
|
+
password?: string;
|
|
17
|
+
database: string;
|
|
18
|
+
}
|
|
19
|
+
/** Parse a `postgres://user:pass@host:port/dbname` connection string. */
|
|
20
|
+
export declare function parseConnectionString(raw: string): ParsedConnectionString;
|
|
21
|
+
export interface DatabaseUrlEngineOptions {
|
|
22
|
+
databaseUrl: string;
|
|
23
|
+
log?: (msg: string) => void;
|
|
24
|
+
}
|
|
25
|
+
export declare function createDatabaseUrlEngine(opts: DatabaseUrlEngineOptions): Promise<DbEngine>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { PgWireClient } from './wire.js';
|
|
2
|
+
import { buildWireEngine } from './wire-engine.js';
|
|
3
|
+
/** Parse a `postgres://user:pass@host:port/dbname` connection string. */
|
|
4
|
+
export function parseConnectionString(raw) {
|
|
5
|
+
let url;
|
|
6
|
+
try {
|
|
7
|
+
url = new URL(raw);
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
throw new Error(`invalid database URL: ${raw}`);
|
|
11
|
+
}
|
|
12
|
+
if (url.protocol !== 'postgres:' && url.protocol !== 'postgresql:') {
|
|
13
|
+
throw new Error(`unsupported database URL scheme "${url.protocol}" (expected postgres:// or postgresql://)`);
|
|
14
|
+
}
|
|
15
|
+
const database = decodeURIComponent(url.pathname.replace(/^\//, ''));
|
|
16
|
+
if (!database)
|
|
17
|
+
throw new Error(`database URL is missing a database name: ${raw}`);
|
|
18
|
+
return {
|
|
19
|
+
host: url.hostname || '127.0.0.1',
|
|
20
|
+
port: url.port ? parseInt(url.port, 10) : 5432,
|
|
21
|
+
user: url.username ? decodeURIComponent(url.username) : 'postgres',
|
|
22
|
+
password: url.password ? decodeURIComponent(url.password) : undefined,
|
|
23
|
+
database,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export async function createDatabaseUrlEngine(opts) {
|
|
27
|
+
const conn = parseConnectionString(opts.databaseUrl);
|
|
28
|
+
opts.log?.(`connecting to external postgres at ${conn.host}:${conn.port}/${conn.database} as ${conn.user}`);
|
|
29
|
+
const connect = () => PgWireClient.connect({
|
|
30
|
+
host: conn.host,
|
|
31
|
+
port: conn.port,
|
|
32
|
+
user: conn.user,
|
|
33
|
+
password: conn.password,
|
|
34
|
+
database: conn.database,
|
|
35
|
+
});
|
|
36
|
+
return buildWireEngine({ connect });
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=database-url.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"database-url.js","sourceRoot":"","sources":["../../../src/node/native/database-url.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AACxC,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AAUlD,yEAAyE;AACzE,MAAM,UAAU,qBAAqB,CAAC,GAAW;IAC/C,IAAI,GAAQ,CAAA;IACZ,IAAI,CAAC;QACH,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAA;IACpB,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CAAC,yBAAyB,GAAG,EAAE,CAAC,CAAA;IACjD,CAAC;IACD,IAAI,GAAG,CAAC,QAAQ,KAAK,WAAW,IAAI,GAAG,CAAC,QAAQ,KAAK,aAAa,EAAE,CAAC;QACnE,MAAM,IAAI,KAAK,CAAC,oCAAoC,GAAG,CAAC,QAAQ,2CAA2C,CAAC,CAAA;IAC9G,CAAC;IACD,MAAM,QAAQ,GAAG,kBAAkB,CAAC,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,CAAA;IACpE,IAAI,CAAC,QAAQ;QAAE,MAAM,IAAI,KAAK,CAAC,4CAA4C,GAAG,EAAE,CAAC,CAAA;IACjF,OAAO;QACL,IAAI,EAAE,GAAG,CAAC,QAAQ,IAAI,WAAW;QACjC,IAAI,EAAE,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI;QAC9C,IAAI,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,kBAAkB,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,UAAU;QAClE,QAAQ,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,kBAAkB,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS;QACrE,QAAQ;KACT,CAAA;AACH,CAAC;AAOD,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAAC,IAA8B;IAC1E,MAAM,IAAI,GAAG,qBAAqB,CAAC,IAAI,CAAC,WAAW,CAAC,CAAA;IACpD,IAAI,CAAC,GAAG,EAAE,CAAC,sCAAsC,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,QAAQ,OAAO,IAAI,CAAC,IAAI,EAAE,CAAC,CAAA;IAC3G,MAAM,OAAO,GAAG,GAAG,EAAE,CACnB,YAAY,CAAC,OAAO,CAAC;QACnB,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,QAAQ,EAAE,IAAI,CAAC,QAAQ;KACxB,CAAC,CAAA;IACJ,OAAO,eAAe,CAAC,EAAE,OAAO,EAAE,CAAC,CAAA;AACrC,CAAC"}
|
|
@@ -11,8 +11,8 @@ import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync
|
|
|
11
11
|
import { writeFile } from 'node:fs/promises';
|
|
12
12
|
import { homedir, tmpdir } from 'node:os';
|
|
13
13
|
import { join } from 'node:path';
|
|
14
|
-
import { Mutex } from '../../db/engine.js';
|
|
15
14
|
import { PgWireClient } from './wire.js';
|
|
15
|
+
import { buildWireEngine } from './wire-engine.js';
|
|
16
16
|
const DEFAULT_PG_VERSION = '17.7.0';
|
|
17
17
|
function target() {
|
|
18
18
|
const arch = process.arch === 'arm64' ? 'aarch64' : process.arch === 'x64' ? 'x86_64' : null;
|
|
@@ -139,55 +139,9 @@ export async function createNativeEngine(opts) {
|
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
141
|
};
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const listeners = new Map();
|
|
146
|
-
listener.onNotification = (channel, payload) => {
|
|
147
|
-
for (const cb of listeners.get(channel) ?? [])
|
|
148
|
-
cb(payload);
|
|
149
|
-
};
|
|
150
|
-
const tx = {
|
|
151
|
-
async query(sql, params) {
|
|
152
|
-
const res = await main.query(sql, normalizeParams(params));
|
|
153
|
-
return { rows: res.rows, affectedRows: res.affectedRows };
|
|
154
|
-
},
|
|
155
|
-
async exec(sql) {
|
|
156
|
-
await main.exec(sql);
|
|
157
|
-
},
|
|
158
|
-
};
|
|
159
|
-
return {
|
|
160
|
-
query(sql, params) {
|
|
161
|
-
return mutex.run(() => tx.query(sql, params));
|
|
162
|
-
},
|
|
163
|
-
exec(sql) {
|
|
164
|
-
return mutex.run(() => tx.exec(sql));
|
|
165
|
-
},
|
|
166
|
-
transaction(fn) {
|
|
167
|
-
return mutex.run(async () => {
|
|
168
|
-
await main.exec('begin');
|
|
169
|
-
try {
|
|
170
|
-
const result = await fn(tx);
|
|
171
|
-
await main.exec('commit');
|
|
172
|
-
return result;
|
|
173
|
-
}
|
|
174
|
-
catch (e) {
|
|
175
|
-
await main.exec('rollback').catch(() => { });
|
|
176
|
-
throw e;
|
|
177
|
-
}
|
|
178
|
-
});
|
|
179
|
-
},
|
|
180
|
-
async listen(channel, cb) {
|
|
181
|
-
if (!listeners.has(channel)) {
|
|
182
|
-
listeners.set(channel, new Set());
|
|
183
|
-
await listener.exec(`listen "${channel.replaceAll('"', '""')}"`);
|
|
184
|
-
}
|
|
185
|
-
listeners.get(channel).add(cb);
|
|
186
|
-
return () => listeners.get(channel)?.delete(cb);
|
|
187
|
-
},
|
|
188
|
-
async close() {
|
|
189
|
-
await main.close().catch(() => { });
|
|
190
|
-
await listener.close().catch(() => { });
|
|
142
|
+
return buildWireEngine({
|
|
143
|
+
connect,
|
|
144
|
+
onClose: async () => {
|
|
191
145
|
if (!childExited) {
|
|
192
146
|
child.kill('SIGINT'); // fast shutdown
|
|
193
147
|
await new Promise((resolve) => {
|
|
@@ -203,7 +157,7 @@ export async function createNativeEngine(opts) {
|
|
|
203
157
|
}
|
|
204
158
|
rmSync(sockDir, { recursive: true, force: true });
|
|
205
159
|
},
|
|
206
|
-
};
|
|
160
|
+
});
|
|
207
161
|
}
|
|
208
162
|
/**
|
|
209
163
|
* Postgres refuses to boot if a postmaster.pid names a live process. When the
|
|
@@ -232,31 +186,4 @@ function removeStalePidFile(pidPath) {
|
|
|
232
186
|
// unreadable pid file — let postgres decide
|
|
233
187
|
}
|
|
234
188
|
}
|
|
235
|
-
/** Match PGlite's param serialization: arrays → pg literals, objects → JSON. */
|
|
236
|
-
function normalizeParams(params) {
|
|
237
|
-
return params?.map((p) => {
|
|
238
|
-
if (p === null || p === undefined)
|
|
239
|
-
return null;
|
|
240
|
-
if (Array.isArray(p))
|
|
241
|
-
return toPgArrayLiteral(p);
|
|
242
|
-
if (p instanceof Date)
|
|
243
|
-
return p.toISOString();
|
|
244
|
-
if (typeof p === 'object')
|
|
245
|
-
return JSON.stringify(p);
|
|
246
|
-
return p;
|
|
247
|
-
});
|
|
248
|
-
}
|
|
249
|
-
function toPgArrayLiteral(arr) {
|
|
250
|
-
const items = arr.map((el) => {
|
|
251
|
-
if (el === null || el === undefined)
|
|
252
|
-
return 'NULL';
|
|
253
|
-
if (Array.isArray(el))
|
|
254
|
-
return toPgArrayLiteral(el);
|
|
255
|
-
if (typeof el === 'number' || typeof el === 'boolean')
|
|
256
|
-
return String(el);
|
|
257
|
-
const s = typeof el === 'object' ? JSON.stringify(el) : String(el);
|
|
258
|
-
return `"${s.replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"`;
|
|
259
|
-
});
|
|
260
|
-
return `{${items.join(',')}}`;
|
|
261
|
-
}
|
|
262
189
|
//# sourceMappingURL=engine.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engine.js","sourceRoot":"","sources":["../../../src/node/native/engine.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,YAAY,EAAE,KAAK,EAAqB,MAAM,oBAAoB,CAAA;AAC3E,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,SAAS,EAAE,WAAW,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,EAAiB,cAAc,EAAE,MAAM,SAAS,CAAA;AACxI,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AACzC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAEhC,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"engine.js","sourceRoot":"","sources":["../../../src/node/native/engine.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds a DbEngine on top of PgWireClient connections. Shared by the embedded
|
|
3
|
+
* native engine (unix socket, trust auth) and the external `--database-url`
|
|
4
|
+
* engine (TCP, password/SCRAM auth) — only the `connect` function differs.
|
|
5
|
+
*/
|
|
6
|
+
import type { DbEngine } from '../../db/engine.js';
|
|
7
|
+
import type { PgWireClient } from './wire.js';
|
|
8
|
+
export interface WireEngineOptions {
|
|
9
|
+
/** Open a fresh connection to the target (socket or TCP). */
|
|
10
|
+
connect: () => Promise<PgWireClient>;
|
|
11
|
+
/** Engine-specific teardown after the connections are closed. */
|
|
12
|
+
onClose?: () => Promise<void> | void;
|
|
13
|
+
}
|
|
14
|
+
export declare function buildWireEngine(opts: WireEngineOptions): Promise<DbEngine>;
|
|
15
|
+
/** Match PGlite's param serialization: arrays → pg literals, objects → JSON. */
|
|
16
|
+
export declare function normalizeParams(params?: unknown[]): unknown[] | undefined;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Mutex } from '../../db/engine.js';
|
|
2
|
+
export async function buildWireEngine(opts) {
|
|
3
|
+
const main = await opts.connect();
|
|
4
|
+
const listener = await opts.connect();
|
|
5
|
+
const mutex = new Mutex();
|
|
6
|
+
const listeners = new Map();
|
|
7
|
+
listener.onNotification = (channel, payload) => {
|
|
8
|
+
for (const cb of listeners.get(channel) ?? [])
|
|
9
|
+
cb(payload);
|
|
10
|
+
};
|
|
11
|
+
const tx = {
|
|
12
|
+
async query(sql, params) {
|
|
13
|
+
const res = await main.query(sql, normalizeParams(params));
|
|
14
|
+
return { rows: res.rows, affectedRows: res.affectedRows };
|
|
15
|
+
},
|
|
16
|
+
async exec(sql) {
|
|
17
|
+
await main.exec(sql);
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
return {
|
|
21
|
+
query(sql, params) {
|
|
22
|
+
return mutex.run(() => tx.query(sql, params));
|
|
23
|
+
},
|
|
24
|
+
exec(sql) {
|
|
25
|
+
return mutex.run(() => tx.exec(sql));
|
|
26
|
+
},
|
|
27
|
+
transaction(fn) {
|
|
28
|
+
return mutex.run(async () => {
|
|
29
|
+
await main.exec('begin');
|
|
30
|
+
try {
|
|
31
|
+
const result = await fn(tx);
|
|
32
|
+
await main.exec('commit');
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
catch (e) {
|
|
36
|
+
await main.exec('rollback').catch(() => { });
|
|
37
|
+
throw e;
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
async listen(channel, cb) {
|
|
42
|
+
if (!listeners.has(channel)) {
|
|
43
|
+
listeners.set(channel, new Set());
|
|
44
|
+
await listener.exec(`listen "${channel.replaceAll('"', '""')}"`);
|
|
45
|
+
}
|
|
46
|
+
listeners.get(channel).add(cb);
|
|
47
|
+
return () => listeners.get(channel)?.delete(cb);
|
|
48
|
+
},
|
|
49
|
+
async close() {
|
|
50
|
+
await main.close().catch(() => { });
|
|
51
|
+
await listener.close().catch(() => { });
|
|
52
|
+
await opts.onClose?.();
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/** Match PGlite's param serialization: arrays → pg literals, objects → JSON. */
|
|
57
|
+
export function normalizeParams(params) {
|
|
58
|
+
return params?.map((p) => {
|
|
59
|
+
if (p === null || p === undefined)
|
|
60
|
+
return null;
|
|
61
|
+
if (Array.isArray(p))
|
|
62
|
+
return toPgArrayLiteral(p);
|
|
63
|
+
if (p instanceof Date)
|
|
64
|
+
return p.toISOString();
|
|
65
|
+
if (typeof p === 'object')
|
|
66
|
+
return JSON.stringify(p);
|
|
67
|
+
return p;
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
function toPgArrayLiteral(arr) {
|
|
71
|
+
const items = arr.map((el) => {
|
|
72
|
+
if (el === null || el === undefined)
|
|
73
|
+
return 'NULL';
|
|
74
|
+
if (Array.isArray(el))
|
|
75
|
+
return toPgArrayLiteral(el);
|
|
76
|
+
if (typeof el === 'number' || typeof el === 'boolean')
|
|
77
|
+
return String(el);
|
|
78
|
+
const s = typeof el === 'object' ? JSON.stringify(el) : String(el);
|
|
79
|
+
return `"${s.replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"`;
|
|
80
|
+
});
|
|
81
|
+
return `{${items.join(',')}}`;
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=wire-engine.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wire-engine.js","sourceRoot":"","sources":["../../../src/node/native/wire-engine.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
interface ConnectOpts {
|
|
2
|
+
socketPath?: string;
|
|
3
|
+
host?: string;
|
|
4
|
+
port?: number;
|
|
5
|
+
user: string;
|
|
6
|
+
database: string;
|
|
7
|
+
password?: string;
|
|
8
|
+
}
|
|
1
9
|
export interface WireResults<T = any> {
|
|
2
10
|
rows: T[];
|
|
3
11
|
affectedRows?: number;
|
|
@@ -16,13 +24,7 @@ export declare class PgWireClient {
|
|
|
16
24
|
private queue;
|
|
17
25
|
private closed;
|
|
18
26
|
onNotification: ((channel: string, payload: string) => void) | null;
|
|
19
|
-
static connect(opts:
|
|
20
|
-
socketPath?: string;
|
|
21
|
-
host?: string;
|
|
22
|
-
port?: number;
|
|
23
|
-
user: string;
|
|
24
|
-
database: string;
|
|
25
|
-
}): Promise<PgWireClient>;
|
|
27
|
+
static connect(opts: ConnectOpts): Promise<PgWireClient>;
|
|
26
28
|
private open;
|
|
27
29
|
/** One protocol op at a time; queued to be safe under concurrency. */
|
|
28
30
|
private run;
|
|
@@ -36,3 +38,4 @@ export declare class PgWireClient {
|
|
|
36
38
|
}
|
|
37
39
|
/** Parse a one-dimensional Postgres array literal: {a,"b c",NULL} */
|
|
38
40
|
export declare function parsePgArray(text: string): (string | null)[];
|
|
41
|
+
export {};
|
package/dist/node/native/wire.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Minimal Postgres wire-protocol (v3) client — enough for tinbase's needs:
|
|
3
|
-
* trust
|
|
4
|
-
* LISTEN/NOTIFY. Pure Node, no
|
|
3
|
+
* trust (unix socket), cleartext/md5/SCRAM-SHA-256 auth (external TCP),
|
|
4
|
+
* simple + extended queries, typed decoding, LISTEN/NOTIFY. Pure Node, no deps.
|
|
5
5
|
*/
|
|
6
6
|
import { createConnection } from 'node:net';
|
|
7
|
+
import { createHash, createHmac, pbkdf2Sync, randomBytes } from 'node:crypto';
|
|
7
8
|
export class PgWireError extends Error {
|
|
8
9
|
code;
|
|
9
10
|
detail;
|
|
@@ -53,6 +54,17 @@ export class PgWireClient {
|
|
|
53
54
|
body.copy(msg, 8);
|
|
54
55
|
this.socket.write(msg);
|
|
55
56
|
});
|
|
57
|
+
// SCRAM handshake state, carried across the SASL message exchange
|
|
58
|
+
let clientNonce = '';
|
|
59
|
+
let clientFirstBare = '';
|
|
60
|
+
let serverSignature = '';
|
|
61
|
+
const needPassword = () => {
|
|
62
|
+
if (opts.password == null) {
|
|
63
|
+
reject(new Error('the server requested a password but none was provided'));
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
return true;
|
|
67
|
+
};
|
|
56
68
|
// Startup phase: consume until first ReadyForQuery
|
|
57
69
|
const startupHandler = (chunk) => {
|
|
58
70
|
this.buffer = Buffer.concat([this.buffer, chunk]);
|
|
@@ -60,10 +72,71 @@ export class PgWireClient {
|
|
|
60
72
|
while ((msg = this.nextMessage()) !== null) {
|
|
61
73
|
const [type, payload] = msg;
|
|
62
74
|
if (type === 0x52) {
|
|
63
|
-
// Authentication
|
|
75
|
+
// Authentication request
|
|
64
76
|
const code = payload.readInt32BE(0);
|
|
65
|
-
if (code
|
|
66
|
-
|
|
77
|
+
if (code === 0) {
|
|
78
|
+
// AuthenticationOk — proceed to ReadyForQuery
|
|
79
|
+
}
|
|
80
|
+
else if (code === 3) {
|
|
81
|
+
// cleartext password
|
|
82
|
+
if (!needPassword())
|
|
83
|
+
return;
|
|
84
|
+
this.socket.write(message(0x70, cstring(opts.password)));
|
|
85
|
+
}
|
|
86
|
+
else if (code === 5) {
|
|
87
|
+
// md5 password: 'md5' + md5(md5(password + user) + salt)
|
|
88
|
+
if (!needPassword())
|
|
89
|
+
return;
|
|
90
|
+
const salt = payload.subarray(4, 8);
|
|
91
|
+
const inner = md5Hex(Buffer.from(opts.password + opts.user, 'utf8'));
|
|
92
|
+
const token = 'md5' + md5Hex(Buffer.concat([Buffer.from(inner, 'utf8'), salt]));
|
|
93
|
+
this.socket.write(message(0x70, cstring(token)));
|
|
94
|
+
}
|
|
95
|
+
else if (code === 10) {
|
|
96
|
+
// SASL: choose SCRAM-SHA-256
|
|
97
|
+
if (!needPassword())
|
|
98
|
+
return;
|
|
99
|
+
const mechs = payload.subarray(4).toString('utf8').split('\0').filter(Boolean);
|
|
100
|
+
if (!mechs.includes('SCRAM-SHA-256')) {
|
|
101
|
+
reject(new Error(`no supported SASL mechanism (server offered: ${mechs.join(', ')})`));
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
clientNonce = randomBytes(18).toString('base64');
|
|
105
|
+
clientFirstBare = `n=,r=${clientNonce}`;
|
|
106
|
+
const initial = Buffer.from(`n,,${clientFirstBare}`, 'utf8');
|
|
107
|
+
this.socket.write(message(0x70, Buffer.concat([cstring('SCRAM-SHA-256'), int32(initial.length), initial])));
|
|
108
|
+
}
|
|
109
|
+
else if (code === 11) {
|
|
110
|
+
// SASLContinue: server-first-message (r=nonce,s=salt,i=iterations)
|
|
111
|
+
const serverFirst = payload.subarray(4).toString('utf8');
|
|
112
|
+
const attrs = scramAttrs(serverFirst);
|
|
113
|
+
if (!attrs.r?.startsWith(clientNonce)) {
|
|
114
|
+
reject(new Error('SCRAM: server nonce does not extend client nonce'));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const salt = Buffer.from(attrs.s, 'base64');
|
|
118
|
+
const iterations = parseInt(attrs.i, 10);
|
|
119
|
+
const saltedPassword = pbkdf2Sync(opts.password, salt, iterations, 32, 'sha256');
|
|
120
|
+
const clientKey = hmac(saltedPassword, 'Client Key');
|
|
121
|
+
const storedKey = sha256(clientKey);
|
|
122
|
+
const finalNoProof = `c=biws,r=${attrs.r}`;
|
|
123
|
+
const authMessage = `${clientFirstBare},${serverFirst},${finalNoProof}`;
|
|
124
|
+
const clientSignature = hmac(storedKey, authMessage);
|
|
125
|
+
const proof = xorBuffers(clientKey, clientSignature);
|
|
126
|
+
serverSignature = hmac(hmac(saltedPassword, 'Server Key'), authMessage).toString('base64');
|
|
127
|
+
const clientFinal = `${finalNoProof},p=${proof.toString('base64')}`;
|
|
128
|
+
this.socket.write(message(0x70, Buffer.from(clientFinal, 'utf8')));
|
|
129
|
+
}
|
|
130
|
+
else if (code === 12) {
|
|
131
|
+
// SASLFinal: verify the server signature (v=...)
|
|
132
|
+
const v = scramAttrs(payload.subarray(4).toString('utf8')).v;
|
|
133
|
+
if (v && serverSignature && v !== serverSignature) {
|
|
134
|
+
reject(new Error('SCRAM: server signature verification failed'));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
reject(new Error(`unsupported auth method ${code}`));
|
|
67
140
|
return;
|
|
68
141
|
}
|
|
69
142
|
}
|
|
@@ -245,6 +318,26 @@ export class PgWireClient {
|
|
|
245
318
|
}
|
|
246
319
|
}
|
|
247
320
|
// ── helpers ───────────────────────────────────────────────────────────────
|
|
321
|
+
// ── auth helpers ──
|
|
322
|
+
const hmac = (key, data) => createHmac('sha256', key).update(data, 'utf8').digest();
|
|
323
|
+
const sha256 = (b) => createHash('sha256').update(b).digest();
|
|
324
|
+
const md5Hex = (b) => createHash('md5').update(b).digest('hex');
|
|
325
|
+
function xorBuffers(a, b) {
|
|
326
|
+
const out = Buffer.alloc(a.length);
|
|
327
|
+
for (let i = 0; i < a.length; i++)
|
|
328
|
+
out[i] = a[i] ^ b[i];
|
|
329
|
+
return out;
|
|
330
|
+
}
|
|
331
|
+
/** Parse SCRAM attribute strings like `r=…,s=…,i=…` into a map. */
|
|
332
|
+
function scramAttrs(s) {
|
|
333
|
+
const out = {};
|
|
334
|
+
for (const part of s.split(',')) {
|
|
335
|
+
const eq = part.indexOf('=');
|
|
336
|
+
if (eq > 0)
|
|
337
|
+
out[part.slice(0, eq)] = part.slice(eq + 1);
|
|
338
|
+
}
|
|
339
|
+
return out;
|
|
340
|
+
}
|
|
248
341
|
function message(type, body) {
|
|
249
342
|
const out = Buffer.alloc(5 + body.length);
|
|
250
343
|
out[0] = type;
|