pgserve 2.3.0 → 2.4.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/bin/pgserve-wrapper.cjs +5 -4
- package/bin/postgres-server.js +142 -631
- package/config/logrotate.d/pgserve +47 -0
- package/config/pgaudit.conf +31 -0
- package/package.json +2 -2
- package/scripts/test-npx.sh +32 -10
- package/src/cli-install.cjs +147 -77
- package/src/commands/uninstall.js +241 -0
- package/src/index.js +11 -44
- package/src/lib/admin-json.js +202 -0
- package/src/lib/pm2-args.js +119 -0
- package/src/lib/socket-dir.js +69 -0
- package/src/postgres.js +64 -5
- package/src/admin-client.js +0 -223
- package/src/audit.js +0 -168
- package/src/cluster.js +0 -654
- package/src/control-db.js +0 -330
- package/src/daemon-control.js +0 -468
- package/src/daemon-shared.js +0 -18
- package/src/daemon-tcp.js +0 -297
- package/src/daemon.js +0 -709
- package/src/dashboard.js +0 -217
- package/src/fingerprint.js +0 -479
- package/src/gc.js +0 -351
- package/src/pg-wire.js +0 -869
- package/src/protocol.js +0 -389
- package/src/restore.js +0 -574
- package/src/router.js +0 -546
- package/src/sdk.js +0 -137
- package/src/stats-collector.js +0 -453
- package/src/stats-dashboard.js +0 -401
- package/src/sync.js +0 -335
- package/src/tenancy.js +0 -75
- package/src/tokens.js +0 -102
package/src/daemon-control.js
DELETED
|
@@ -1,468 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* pgserve daemon — Unix control-socket accept path (Group 2 + Group 4).
|
|
3
|
-
*
|
|
4
|
-
* Accepted on `$XDG_RUNTIME_DIR/pgserve/control.sock`, peers identify via
|
|
5
|
-
* SO_PEERCRED → /proc walk → package.json hash, then route into the
|
|
6
|
-
* fingerprint's tenant database. The plain TCP path lives in
|
|
7
|
-
* `daemon-tcp.js` and shares the same prototype owner.
|
|
8
|
-
*
|
|
9
|
-
* Methods are attached to `PgserveDaemon.prototype` from `daemon.js` so
|
|
10
|
-
* the surface is one cohesive class — splitting modules here is purely
|
|
11
|
-
* to honour the 1000-line discipline (AGENTS.md §8).
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
/* global Bun */
|
|
15
|
-
import fs from 'fs';
|
|
16
|
-
import { extractDatabaseName, rewriteDatabaseName, buildErrorResponse } from './protocol.js';
|
|
17
|
-
import { handleControlAccept, readPersistFlag } from './fingerprint.js';
|
|
18
|
-
import { audit, AUDIT_EVENTS } from './audit.js';
|
|
19
|
-
import {
|
|
20
|
-
findRowByFingerprint,
|
|
21
|
-
recordDbCreated,
|
|
22
|
-
touchLastConnection,
|
|
23
|
-
markPersist,
|
|
24
|
-
} from './control-db.js';
|
|
25
|
-
import {
|
|
26
|
-
resolveTenantDatabaseName,
|
|
27
|
-
} from './tenancy.js';
|
|
28
|
-
import { flushPending } from './daemon-shared.js';
|
|
29
|
-
|
|
30
|
-
const PROTOCOL_VERSION_3 = 196608;
|
|
31
|
-
const SSL_REQUEST_CODE = 80877103;
|
|
32
|
-
const GSSAPI_REQUEST_CODE = 80877104;
|
|
33
|
-
const CANCEL_REQUEST_CODE = 80877102;
|
|
34
|
-
|
|
35
|
-
const MAX_STARTUP_BUFFER_SIZE = 1024 * 1024; // 1 MiB — same bound as router.js
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Install the Unix control-socket handlers on PgserveDaemon.prototype.
|
|
39
|
-
* Called once from daemon.js at module load.
|
|
40
|
-
*/
|
|
41
|
-
export function attachControlHandlers(PgserveDaemon) {
|
|
42
|
-
PgserveDaemon.prototype.handleSocketOpen = handleSocketOpen;
|
|
43
|
-
PgserveDaemon.prototype.handleSocketData = handleSocketData;
|
|
44
|
-
PgserveDaemon.prototype.processStartupMessage = processStartupMessage;
|
|
45
|
-
PgserveDaemon.prototype.handleSocketClose = handleSocketClose;
|
|
46
|
-
PgserveDaemon.prototype.handleSocketError = handleSocketError;
|
|
47
|
-
PgserveDaemon.prototype.resolveTenantDatabase = resolveTenantDatabase;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Per-accept fingerprint derivation is live: SO_PEERCRED → /proc walk →
|
|
52
|
-
* package.json hash. Fingerprint info is parked on socketState so the
|
|
53
|
-
* tenant lookup in processStartupMessage doesn't re-derive on every byte.
|
|
54
|
-
*/
|
|
55
|
-
function handleSocketOpen(socket) {
|
|
56
|
-
let fingerprint = null;
|
|
57
|
-
try {
|
|
58
|
-
const opts = this._fingerprintAcceptOpts ? (this._fingerprintAcceptOpts(socket) || {}) : {};
|
|
59
|
-
fingerprint = handleControlAccept(socket, opts);
|
|
60
|
-
} catch (err) {
|
|
61
|
-
this.logger.warn?.(
|
|
62
|
-
{ err: err?.message || String(err) },
|
|
63
|
-
'Failed to derive peer fingerprint on accept',
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
this.socketState.set(socket, {
|
|
67
|
-
buffer: null,
|
|
68
|
-
pgSocket: null,
|
|
69
|
-
dbName: null,
|
|
70
|
-
handshakeComplete: false,
|
|
71
|
-
startupInProgress: false,
|
|
72
|
-
pendingToPg: null,
|
|
73
|
-
pendingToClient: null,
|
|
74
|
-
fingerprint,
|
|
75
|
-
// Wall-clock timestamp when this socket was accepted. The watchdog
|
|
76
|
-
// installed by PgserveDaemon.start() forcibly closes any socket that
|
|
77
|
-
// hasn't completed its postgres handshake within
|
|
78
|
-
// PGSERVE_HANDSHAKE_DEADLINE_MS. Without this, a peer that connects
|
|
79
|
-
// and never sends the StartupMessage occupies the connection slot
|
|
80
|
-
// forever — the file-descriptor leak documented in pgserve#45.
|
|
81
|
-
acceptedAt: Date.now(),
|
|
82
|
-
});
|
|
83
|
-
this.connections.add(socket);
|
|
84
|
-
if (fingerprint) {
|
|
85
|
-
this.emit('accept', { fingerprint, socket });
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function handleSocketData(socket, data) {
|
|
90
|
-
const state = this.socketState.get(socket);
|
|
91
|
-
if (!state) return;
|
|
92
|
-
|
|
93
|
-
if (state.handshakeComplete && state.pgSocket) {
|
|
94
|
-
if (state.pendingToPg) {
|
|
95
|
-
state.pendingToPg = Buffer.concat([state.pendingToPg, Buffer.from(data)]);
|
|
96
|
-
socket.pause();
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
const written = state.pgSocket.write(data);
|
|
100
|
-
if (written < data.byteLength) {
|
|
101
|
-
state.pendingToPg = written === 0 ? Buffer.from(data) : Buffer.from(data.subarray(written));
|
|
102
|
-
socket.pause();
|
|
103
|
-
}
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const incomingSize = state.buffer ? state.buffer.length + data.byteLength : data.byteLength;
|
|
108
|
-
if (incomingSize > MAX_STARTUP_BUFFER_SIZE) {
|
|
109
|
-
this.logger.warn?.(
|
|
110
|
-
{ incomingSize, limit: MAX_STARTUP_BUFFER_SIZE },
|
|
111
|
-
'Pre-handshake buffer exceeded limit — closing connection',
|
|
112
|
-
);
|
|
113
|
-
socket.end();
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
if (state.buffer) {
|
|
117
|
-
state.buffer = Buffer.concat([state.buffer, Buffer.from(data)]);
|
|
118
|
-
} else {
|
|
119
|
-
state.buffer = Buffer.from(data);
|
|
120
|
-
}
|
|
121
|
-
this.processStartupMessage(socket, state).catch((err) => {
|
|
122
|
-
this.logger.error?.({ err: err.message }, 'processStartupMessage failed');
|
|
123
|
-
try { socket.end(); } catch { /* swallow */ }
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
async function processStartupMessage(socket, state) {
|
|
128
|
-
if (state.startupInProgress) return;
|
|
129
|
-
const buffer = state.buffer;
|
|
130
|
-
if (!buffer || buffer.length < 8) return;
|
|
131
|
-
|
|
132
|
-
const messageLength = buffer.readUInt32BE(0);
|
|
133
|
-
if (buffer.length < messageLength) return;
|
|
134
|
-
|
|
135
|
-
const code = buffer.readUInt32BE(4);
|
|
136
|
-
|
|
137
|
-
if (code === SSL_REQUEST_CODE || code === GSSAPI_REQUEST_CODE) {
|
|
138
|
-
socket.write(Buffer.from('N'));
|
|
139
|
-
state.buffer = buffer.length > messageLength ? buffer.subarray(messageLength) : null;
|
|
140
|
-
if (state.buffer) await processStartupMessage.call(this, socket, state);
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (code === CANCEL_REQUEST_CODE) {
|
|
145
|
-
socket.end();
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if (code !== PROTOCOL_VERSION_3) {
|
|
150
|
-
this.logger.warn?.({ code }, 'Unsupported protocol version on control socket');
|
|
151
|
-
socket.end();
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const startupMessage = buffer.subarray(0, messageLength);
|
|
156
|
-
const requestedDb = extractDatabaseName(startupMessage);
|
|
157
|
-
state.dbName = requestedDb;
|
|
158
|
-
state.startupInProgress = true;
|
|
159
|
-
|
|
160
|
-
try {
|
|
161
|
-
const resolution = await this.resolveTenantDatabase(state, requestedDb);
|
|
162
|
-
if (resolution.deny) {
|
|
163
|
-
const errFrame = buildErrorResponse({
|
|
164
|
-
severity: 'FATAL',
|
|
165
|
-
sqlstate: '28P01',
|
|
166
|
-
message: resolution.message,
|
|
167
|
-
});
|
|
168
|
-
try {
|
|
169
|
-
// Bun's TCPSocket.end(data) writes then closes atomically — using
|
|
170
|
-
// write()+end() can race the FIN past the data on some kernels and
|
|
171
|
-
// leave the peer waiting for AuthOK indefinitely.
|
|
172
|
-
socket.end(errFrame);
|
|
173
|
-
} catch { /* swallow */ }
|
|
174
|
-
this.emit('connection-denied', {
|
|
175
|
-
fingerprint: state.fingerprint?.fingerprint || null,
|
|
176
|
-
requested: requestedDb,
|
|
177
|
-
owned: resolution.ownedDatabaseName,
|
|
178
|
-
});
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const dbName = resolution.databaseName;
|
|
183
|
-
state.dbName = dbName;
|
|
184
|
-
const outgoingStartup = (dbName !== requestedDb)
|
|
185
|
-
? rewriteDatabaseName(startupMessage, dbName)
|
|
186
|
-
: startupMessage;
|
|
187
|
-
|
|
188
|
-
if (this.autoProvision) {
|
|
189
|
-
await this.pgManager.createDatabase(dbName);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const pgSocketPath = this.pgManager.getSocketPath();
|
|
193
|
-
const daemon = this;
|
|
194
|
-
const pgHandler = {
|
|
195
|
-
data(_pgSocket, pgData) {
|
|
196
|
-
if (state.pendingToClient) {
|
|
197
|
-
state.pendingToClient = Buffer.concat([state.pendingToClient, Buffer.from(pgData)]);
|
|
198
|
-
_pgSocket.pause();
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
const written = socket.write(pgData);
|
|
202
|
-
if (written < pgData.byteLength) {
|
|
203
|
-
state.pendingToClient = written === 0
|
|
204
|
-
? Buffer.from(pgData)
|
|
205
|
-
: Buffer.from(pgData.subarray(written));
|
|
206
|
-
_pgSocket.pause();
|
|
207
|
-
}
|
|
208
|
-
},
|
|
209
|
-
open(pgSocket) {
|
|
210
|
-
pgSocket.write(outgoingStartup);
|
|
211
|
-
state.handshakeComplete = true;
|
|
212
|
-
},
|
|
213
|
-
close() {
|
|
214
|
-
try { socket.end(); } catch { /* swallow */ }
|
|
215
|
-
},
|
|
216
|
-
error(_pgSocket, error) {
|
|
217
|
-
daemon.logger.error?.({ dbName, err: error?.message || String(error) }, 'PG-side proxy socket error');
|
|
218
|
-
try { socket.end(); } catch { /* swallow */ }
|
|
219
|
-
},
|
|
220
|
-
drain(_pgSocket) {
|
|
221
|
-
if (state.pendingToPg) {
|
|
222
|
-
state.pendingToPg = flushPending(_pgSocket, state.pendingToPg);
|
|
223
|
-
}
|
|
224
|
-
if (!state.pendingToPg) {
|
|
225
|
-
socket.resume();
|
|
226
|
-
}
|
|
227
|
-
},
|
|
228
|
-
};
|
|
229
|
-
|
|
230
|
-
// Same #24 safety net as the router: socketPath might point at a
|
|
231
|
-
// directory the PG manager has since cleaned up. Fall back to TCP
|
|
232
|
-
// rather than hanging on a missing socket file.
|
|
233
|
-
//
|
|
234
|
-
// Single-retry-with-backoff: if the first connect attempt fails (the
|
|
235
|
-
// backend is mid-restart, OOM-recovering, etc.), wait
|
|
236
|
-
// BACKEND_CONNECT_RETRY_DELAY_MS and try once more before giving up.
|
|
237
|
-
// On final failure, send the client a postgres ErrorResponse with
|
|
238
|
-
// SQLSTATE 57P03 (cannot_connect_now) so libpq clients can distinguish
|
|
239
|
-
// "transient backend unavailability" from real auth/network errors —
|
|
240
|
-
// pgserve#45 noted that the previous "buffer forever" path was the
|
|
241
|
-
// worst possible outcome.
|
|
242
|
-
state.pgSocket = await connectBackendWithRetry({
|
|
243
|
-
pgSocketPath,
|
|
244
|
-
pgPort: this.pgManager.port,
|
|
245
|
-
pgHandler,
|
|
246
|
-
logger: this.logger,
|
|
247
|
-
dbName,
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
this.emit('connection', { dbName, socket });
|
|
251
|
-
} catch (err) {
|
|
252
|
-
this.logger.error?.({ dbName: state.dbName, err: err?.message || String(err) }, 'Daemon connection error');
|
|
253
|
-
// Tell the client why we're closing rather than just dropping the
|
|
254
|
-
// socket — silent drops were one of the recovery footguns documented
|
|
255
|
-
// in pgserve#45. 57P03 = cannot_connect_now (Postgres standard).
|
|
256
|
-
try {
|
|
257
|
-
const errFrame = buildErrorResponse({
|
|
258
|
-
severity: 'FATAL',
|
|
259
|
-
sqlstate: '57P03',
|
|
260
|
-
message: 'backend unavailable, retry shortly',
|
|
261
|
-
});
|
|
262
|
-
// socket.end(data) writes-then-closes atomically; same idempotent
|
|
263
|
-
// pattern used for the 28P01 deny branch above.
|
|
264
|
-
socket.end(errFrame);
|
|
265
|
-
} catch { /* swallow */ }
|
|
266
|
-
this.emit('connection-error', { error: err, dbName: state.dbName });
|
|
267
|
-
} finally {
|
|
268
|
-
state.startupInProgress = false;
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
const BACKEND_CONNECT_RETRY_DELAY_MS = 200;
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* Connect to the postgres backend with one retry on failure. Honours the
|
|
276
|
-
* existing `useUnix vs TCP fallback` policy (PR #24 safety net): every
|
|
277
|
-
* attempt re-checks whether the Unix socket path still exists, because the
|
|
278
|
-
* PG manager may have nulled it between attempts.
|
|
279
|
-
*
|
|
280
|
-
* Throws the final connect error after the retry; callers translate that
|
|
281
|
-
* into a 57P03 ErrorResponse for the client.
|
|
282
|
-
*/
|
|
283
|
-
async function connectBackendWithRetry({ pgSocketPath, pgPort, pgHandler, logger, dbName }) {
|
|
284
|
-
const tryOnce = async () => {
|
|
285
|
-
const useUnix = pgSocketPath && fs.existsSync(pgSocketPath);
|
|
286
|
-
if (useUnix) {
|
|
287
|
-
return await Bun.connect({ unix: pgSocketPath, socket: pgHandler });
|
|
288
|
-
}
|
|
289
|
-
if (pgSocketPath && !useUnix) {
|
|
290
|
-
logger?.warn?.({ pgSocketPath, dbName }, 'PG Unix socket path stale — falling back to TCP');
|
|
291
|
-
}
|
|
292
|
-
return await Bun.connect({
|
|
293
|
-
hostname: '127.0.0.1',
|
|
294
|
-
port: pgPort,
|
|
295
|
-
socket: pgHandler,
|
|
296
|
-
});
|
|
297
|
-
};
|
|
298
|
-
|
|
299
|
-
try {
|
|
300
|
-
return await tryOnce();
|
|
301
|
-
} catch (firstErr) {
|
|
302
|
-
logger?.warn?.(
|
|
303
|
-
{ dbName, err: firstErr?.message || String(firstErr), retryAfterMs: BACKEND_CONNECT_RETRY_DELAY_MS },
|
|
304
|
-
'Backend connect failed — retrying once',
|
|
305
|
-
);
|
|
306
|
-
await new Promise((r) => setTimeout(r, BACKEND_CONNECT_RETRY_DELAY_MS));
|
|
307
|
-
return await tryOnce();
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
/**
|
|
312
|
-
* Group 4 — wire identity to tenancy.
|
|
313
|
-
*
|
|
314
|
-
* On first connect: provision a per-fingerprint database, audit the create.
|
|
315
|
-
* On reconnect: bump last_connection_at + liveness_pid.
|
|
316
|
-
* On cross-tenant attempt: deny with SQLSTATE 28P01, OR (when the kill
|
|
317
|
-
* switch is on) bypass and audit `enforcement_kill_switch_used`.
|
|
318
|
-
*/
|
|
319
|
-
async function resolveTenantDatabase(state, requestedDb) {
|
|
320
|
-
const fp = state.fingerprint;
|
|
321
|
-
// No fingerprint (FFI unavailable, accept hook failed) or no admin
|
|
322
|
-
// client (init failed) → behave as v1: route the requested name through
|
|
323
|
-
// unchanged. Tenancy enforcement is best-effort, never load-bearing for
|
|
324
|
-
// basic connectivity.
|
|
325
|
-
if (!fp || !this._adminClient) {
|
|
326
|
-
return { databaseName: requestedDb };
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
const { fingerprint, name, uid, pid, packageRealpath } = fp;
|
|
330
|
-
const lookupOpts = { timeoutMs: this.adminLookupTimeoutMs };
|
|
331
|
-
|
|
332
|
-
let row = null;
|
|
333
|
-
try {
|
|
334
|
-
row = await findRowByFingerprint(this._adminClient, fingerprint, lookupOpts);
|
|
335
|
-
} catch (err) {
|
|
336
|
-
this.logger.warn?.(
|
|
337
|
-
{ err: err?.message || String(err), fingerprint },
|
|
338
|
-
'pgserve_meta lookup failed — falling back to requested DB',
|
|
339
|
-
);
|
|
340
|
-
return { databaseName: requestedDb };
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
// Group 5: read pgserve.persist from the resolved package.json so the row
|
|
344
|
-
// we (re)write reflects the peer's lifecycle preference. Script-mode peers
|
|
345
|
-
// never get persist=true (no package.json to opt in from).
|
|
346
|
-
const persistRequested = packageRealpath ? readPersistFlag(packageRealpath) : false;
|
|
347
|
-
|
|
348
|
-
if (!row) {
|
|
349
|
-
const newName = resolveTenantDatabaseName({ name, fingerprint });
|
|
350
|
-
try {
|
|
351
|
-
await this.pgManager.createDatabase(newName);
|
|
352
|
-
await recordDbCreated(this._adminClient, {
|
|
353
|
-
databaseName: newName,
|
|
354
|
-
fingerprint,
|
|
355
|
-
peerUid: typeof uid === 'number' ? uid : -1,
|
|
356
|
-
packageRealpath: packageRealpath || null,
|
|
357
|
-
livenessPid: typeof pid === 'number' && pid > 0 ? pid : null,
|
|
358
|
-
persist: persistRequested,
|
|
359
|
-
}, lookupOpts);
|
|
360
|
-
audit(AUDIT_EVENTS.DB_CREATED, {
|
|
361
|
-
database: newName,
|
|
362
|
-
fingerprint,
|
|
363
|
-
peer_uid: uid,
|
|
364
|
-
peer_pid: pid,
|
|
365
|
-
package_realpath: packageRealpath || null,
|
|
366
|
-
name,
|
|
367
|
-
persist: persistRequested,
|
|
368
|
-
});
|
|
369
|
-
} catch (err) {
|
|
370
|
-
this.logger.error?.(
|
|
371
|
-
{ err: err?.message || String(err), fingerprint, dbName: newName },
|
|
372
|
-
'Failed to provision per-fingerprint database',
|
|
373
|
-
);
|
|
374
|
-
throw err;
|
|
375
|
-
}
|
|
376
|
-
row = { databaseName: newName, fingerprint, peerUid: uid, allowedTokens: [] };
|
|
377
|
-
} else {
|
|
378
|
-
try {
|
|
379
|
-
await touchLastConnection(this._adminClient, {
|
|
380
|
-
databaseName: row.databaseName,
|
|
381
|
-
livenessPid: typeof pid === 'number' && pid > 0 ? pid : null,
|
|
382
|
-
}, lookupOpts);
|
|
383
|
-
} catch (err) {
|
|
384
|
-
this.logger.warn?.(
|
|
385
|
-
{ err: err?.message || String(err), database: row.databaseName },
|
|
386
|
-
'touchLastConnection failed (non-fatal)',
|
|
387
|
-
);
|
|
388
|
-
}
|
|
389
|
-
// Group 5: keep persist in sync when the peer's package.json toggles the
|
|
390
|
-
// flag between connections — the previous run might have started without
|
|
391
|
-
// persist:true and the operator just added it (or vice versa).
|
|
392
|
-
try {
|
|
393
|
-
await markPersist(this._adminClient, row.databaseName, persistRequested, lookupOpts);
|
|
394
|
-
} catch (err) {
|
|
395
|
-
this.logger.warn?.(
|
|
396
|
-
{ err: err?.message || String(err), database: row.databaseName },
|
|
397
|
-
'markPersist failed (non-fatal)',
|
|
398
|
-
);
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
// Enforcement: peer asked for an explicit database that isn't theirs.
|
|
403
|
-
// libpq's default `database = user` is treated the same as `postgres` —
|
|
404
|
-
// both are "I don't care, give me whatever you have for me", so we
|
|
405
|
-
// silently route them into the fingerprint's DB.
|
|
406
|
-
const requested = (typeof requestedDb === 'string' && requestedDb.length > 0)
|
|
407
|
-
? requestedDb
|
|
408
|
-
: 'postgres';
|
|
409
|
-
const isImplicit = requested === 'postgres' || requested === row.databaseName;
|
|
410
|
-
if (!isImplicit) {
|
|
411
|
-
if (this.enforcementDisabled) {
|
|
412
|
-
audit(AUDIT_EVENTS.ENFORCEMENT_KILL_SWITCH_USED, {
|
|
413
|
-
fingerprint,
|
|
414
|
-
peer_uid: uid,
|
|
415
|
-
peer_pid: pid,
|
|
416
|
-
requested_database: requested,
|
|
417
|
-
owned_database: row.databaseName,
|
|
418
|
-
});
|
|
419
|
-
try { await this.pgManager.createDatabase(requested); } catch { /* swallow */ }
|
|
420
|
-
return { databaseName: requested };
|
|
421
|
-
}
|
|
422
|
-
audit(AUDIT_EVENTS.CONNECTION_DENIED_FINGERPRINT_MISMATCH, {
|
|
423
|
-
fingerprint,
|
|
424
|
-
peer_uid: uid,
|
|
425
|
-
peer_pid: pid,
|
|
426
|
-
requested_database: requested,
|
|
427
|
-
owned_database: row.databaseName,
|
|
428
|
-
});
|
|
429
|
-
return {
|
|
430
|
-
deny: true,
|
|
431
|
-
ownedDatabaseName: row.databaseName,
|
|
432
|
-
message:
|
|
433
|
-
`database fingerprint mismatch: peer ${fingerprint} owns ` +
|
|
434
|
-
`${row.databaseName}, requested ${requested}`,
|
|
435
|
-
};
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
return { databaseName: row.databaseName };
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
function handleSocketClose(socket) {
|
|
442
|
-
const state = this.socketState.get(socket);
|
|
443
|
-
if (state) {
|
|
444
|
-
state.pendingToPg = null;
|
|
445
|
-
state.pendingToClient = null;
|
|
446
|
-
if (state.pgSocket) {
|
|
447
|
-
try { state.pgSocket.end(); } catch { /* swallow */ }
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
this.connections.delete(socket);
|
|
451
|
-
this.socketState.delete(socket);
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
function handleSocketError(socket, error) {
|
|
455
|
-
const state = this.socketState.get(socket);
|
|
456
|
-
if (error?.code !== 'ECONNRESET') {
|
|
457
|
-
this.logger.error?.({ err: error?.message || String(error), dbName: state?.dbName }, 'Control socket error');
|
|
458
|
-
}
|
|
459
|
-
if (state) {
|
|
460
|
-
state.pendingToPg = null;
|
|
461
|
-
state.pendingToClient = null;
|
|
462
|
-
if (state.pgSocket) {
|
|
463
|
-
try { state.pgSocket.end(); } catch { /* swallow */ }
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
this.connections.delete(socket);
|
|
467
|
-
this.socketState.delete(socket);
|
|
468
|
-
}
|
package/src/daemon-shared.js
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared helpers for the Unix-socket and TCP accept paths.
|
|
3
|
-
*
|
|
4
|
-
* Kept as a tiny module so daemon.js stays under 1000 lines (AGENTS.md §8)
|
|
5
|
-
* without forcing a circular dep between daemon-control.js and daemon-tcp.js.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Drain the buffered tail to a Bun socket. Returns the still-pending tail
|
|
10
|
-
* (or null when fully flushed). Same shape as the original inline helper
|
|
11
|
-
* in daemon.js.
|
|
12
|
-
*/
|
|
13
|
-
export function flushPending(target, pending) {
|
|
14
|
-
const written = target.write(pending);
|
|
15
|
-
if (written === pending.byteLength) return null;
|
|
16
|
-
if (written === 0) return pending;
|
|
17
|
-
return pending.subarray(written);
|
|
18
|
-
}
|