pgserve 1.1.10 → 1.2.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/SECURITY.md ADDED
@@ -0,0 +1,109 @@
1
+ # Security Policy
2
+
3
+ `pgserve` is maintained by [Automagik](https://automagik.dev). We take the security of this package seriously and appreciate responsible disclosure from the community.
4
+
5
+ ---
6
+
7
+ ## Reporting a Vulnerability
8
+
9
+ **Please do not open public issues for security reports.**
10
+
11
+ Send private reports to one of the following channels:
12
+
13
+ | Channel | Address | Best for |
14
+ |---------|---------|----------|
15
+ | Security email | `privacidade@namastex.ai` | Anything security-related, including coordinated disclosure |
16
+ | DPO (privacy + security officer) | `dpo@khal.ai` | Privacy, LGPD, data protection concerns |
17
+ | Private GitHub advisory | [Report via GitHub](https://github.com/namastexlabs/pgserve/security/advisories/new) | Preferred for CVE assignment and coordinated release |
18
+
19
+ **PGP** available on request.
20
+
21
+ ### Response SLA
22
+
23
+ - Acknowledgement: **within 2 business hours** (UTC-3).
24
+ - Initial triage and severity assessment: **within 24 hours**.
25
+ - Fix or mitigation plan: **within 7 days** for critical/high severity.
26
+ - Public disclosure: coordinated with reporter, typically within 30 days of fix.
27
+
28
+ We will credit reporters publicly (with their permission) in the released advisory.
29
+
30
+ ---
31
+
32
+ ## Supported Versions
33
+
34
+ | Version line | Status |
35
+ |--------------|--------|
36
+ | `1.1.10` and later clean releases | ✅ Supported — current |
37
+ | `1.1.11` – `1.1.14` | ❌ **COMPROMISED — do not use** |
38
+ | `1.1.0` – `1.1.9` | ⚠️ Legacy — security patches only |
39
+ | `1.0.x` and earlier | ❌ End of life |
40
+
41
+ Always install from the current stable line. Pin explicit versions in your `package.json` and avoid `latest` for supply-chain sensitive packages.
42
+
43
+ ---
44
+
45
+ ## Past Incidents
46
+
47
+ ### 2026-04 — CanisterWorm supply-chain compromise
48
+
49
+ Between 2026-04-21 (~22:14 UTC) and 2026-04-22 (~14:00 UTC), versions `1.1.11`, `1.1.12`, `1.1.13`, and `1.1.14` were published to npm by a threat actor after a developer GitHub OAuth token was exfiltrated. The malicious versions contained a `TeamPCP` payload in `scripts/check-env.js` that executed via `postinstall` to harvest local credentials.
50
+
51
+ - **Exposure window:** ~16 hours
52
+ - **Detection-to-containment:** under 20 hours
53
+ - **Current status:** malicious versions `npm unpublish`-ed and no longer installable
54
+
55
+ **If you installed versions `1.1.11` – `1.1.14` between April 21–22, 2026, assume your machine is compromised.** Follow the remediation guide linked below.
56
+
57
+ **Resources:**
58
+ - 📖 [Full incident response manual](https://github.com/namastexlabs/genie-dpo/blob/main/knowledge/canisterworm-incident-response.md)
59
+ - 🌐 [Public advisory (English)](https://automagik.dev/security)
60
+ - 🌐 [Aviso público (Português)](https://automagik.dev/seguranca)
61
+ - 🛡️ [GitHub Security Advisories](https://github.com/namastexlabs/pgserve/security/advisories) for this repository
62
+
63
+ A full public post-mortem will be published within 30 days of containment.
64
+
65
+ ---
66
+
67
+ ## Acknowledgments
68
+
69
+ We thank the researchers and organizations that identified and tracked this incident:
70
+
71
+ - [**Socket Research Team**](https://socket.dev/blog/namastex-npm-packages-compromised-canisterworm) — primary discovery and continued tracking at [socket.dev/supply-chain-attacks/canistersprawl](https://socket.dev/supply-chain-attacks/canistersprawl).
72
+ - **Endor Labs**, **Kodem Security**, **BleepingComputer**, **The Register**, **CSO Online**, **GBHackers**, **Cybersecurity News** — for coverage, analysis, and technical breakdowns that helped defenders respond quickly.
73
+
74
+ We also thank the Automagik team that ran the end-to-end response during the incident window, and the broader open-source community whose scrutiny, tools, and unfiltered feedback keep this ecosystem healthy. We will keep earning it.
75
+
76
+ ---
77
+
78
+ ## Our Commitments
79
+
80
+ Effective 2026-04-23, all `pgserve` releases are governed by:
81
+
82
+ - **Provenance attestation** — every publication is signed with `npm --provenance` and verifiable via Sigstore.
83
+ - **OIDC trusted publishing** — migrating to GitHub Actions OIDC publish, eliminating long-lived npm tokens. (in progress)
84
+ - **Mandatory 2FA** on every maintainer account with publish rights.
85
+ - **Environment protection** — production publishes require manual approval from a second maintainer.
86
+ - **Quarterly token audit** — scope and permission review.
87
+ - **External pentest** — scheduled ahead of the original roadmap.
88
+
89
+ ---
90
+
91
+ ## Hardening Recommendations for Consumers
92
+
93
+ - Pin explicit versions, not `latest`: `"pgserve": "1.1.10"`.
94
+ - Use `npm ci` in CI. It enforces lockfile-based installs by default.
95
+ - Evaluate `--ignore-scripts` per-package for untrusted dependencies. The current `pgserve` release does not require any lifecycle script to function.
96
+ - Verify package provenance: `npm view pgserve --json | jq '.dist.attestations'`.
97
+ - Monitor advisories: subscribe to GitHub security alerts for this repository.
98
+
99
+ ---
100
+
101
+ ## Contact
102
+
103
+ - **Security & incidents:** `privacidade@namastex.ai`
104
+ - **Data Protection Officer (DPO):** Cezar Vasconcelos — `dpo@khal.ai`
105
+ - **Security disclosure page:** [automagik.dev/security](https://automagik.dev/security)
106
+
107
+ Namastex Labs Serviços em Tecnologia Ltda · CNPJ 46.156.854/0001-62
108
+
109
+ *Last updated: 2026-04-23 · v1.0*
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pgserve",
3
- "version": "1.1.10",
3
+ "version": "1.2.0",
4
4
  "description": "Embedded PostgreSQL server with true concurrent connections - zero config, auto-provision databases",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
package/src/postgres.js CHANGED
@@ -444,8 +444,20 @@ export class PostgresManager {
444
444
 
445
445
  /**
446
446
  * Start the embedded PostgreSQL instance
447
+ *
448
+ * Re-entry guard: if a previous start() left `this.process` or stale state
449
+ * behind, refuse silently rather than leaking another socketDir/databaseDir.
450
+ * Callers must call stop() first if they want to restart.
447
451
  */
448
452
  async start() {
453
+ if (this.process) {
454
+ this.logger?.warn(
455
+ { pid: this.process.pid, socketDir: this.socketDir },
456
+ 'PostgresManager.start() called while already started — returning existing instance'
457
+ );
458
+ return this;
459
+ }
460
+
449
461
  // Get binary paths (may extract bundled binaries on first run)
450
462
  this.binaries = await getBinaryPaths();
451
463
 
@@ -773,12 +785,33 @@ export class PostgresManager {
773
785
  readStream(this.process.stdout);
774
786
 
775
787
  // Handle process exit
788
+ //
789
+ // When the postgres subprocess exits (normal stop OR crash), we must
790
+ // null `this.process` AND `this.socketDir`/`this.databaseDir` so that
791
+ // subsequent `getSocketPath()` calls do not return a path to a directory
792
+ // that no longer exists. This is the issue #24 root cause: the router
793
+ // was receiving stale socketPaths pointing to cleaned-up tmp dirs.
794
+ //
795
+ // NOTE: we do NOT null socketDir here if `stop()` is in flight, because
796
+ // stop() already handles cleanup+null. We only need to self-heal when
797
+ // the exit is unexpected (external kill, crash, OOM).
776
798
  this.process.exited.then((code) => {
777
799
  processExited = true;
778
800
  if (!started) {
779
801
  reject(new Error(`PostgreSQL exited with code ${code} before starting: ${startupOutput}`));
780
802
  }
781
803
  this.process = null;
804
+ // On unexpected exit (not via stop()), reset cached paths so that
805
+ // getSocketPath() returns null and callers can fall back to TCP
806
+ // or force a fresh start().
807
+ if (!this._stopping) {
808
+ this.socketDir = null;
809
+ this.databaseDir = null;
810
+ this.logger?.warn(
811
+ { code },
812
+ 'PostgreSQL subprocess exited unexpectedly — socketDir/databaseDir reset'
813
+ );
814
+ }
782
815
  });
783
816
 
784
817
  // Method 1: TCP connection polling (preferred, works on Linux/macOS)
@@ -1294,8 +1327,19 @@ export class PostgresManager {
1294
1327
 
1295
1328
  /**
1296
1329
  * Stop the PostgreSQL instance
1330
+ *
1331
+ * Cleanup order matters: we null `this.socketDir`/`this.databaseDir` AFTER
1332
+ * the rmSync so any concurrent `getSocketPath()` call either sees the old
1333
+ * path (while it still exists) or null (after cleanup) — never a path
1334
+ * pointing to a deleted directory.
1335
+ *
1336
+ * The `_stopping` flag tells the process.exited handler to NOT redundantly
1337
+ * null the paths (avoids a race where start() called immediately after
1338
+ * stop() sees nulls that stop() was about to set anyway).
1297
1339
  */
1298
1340
  async stop() {
1341
+ this._stopping = true;
1342
+
1299
1343
  // Close admin pool first (Bun.sql)
1300
1344
  if (this.adminPool) {
1301
1345
  await this.adminPool.close();
@@ -1340,6 +1384,16 @@ export class PostgresManager {
1340
1384
  }
1341
1385
  }
1342
1386
  }
1387
+
1388
+ // Reset cached paths UNCONDITIONALLY after cleanup so getSocketPath()
1389
+ // returns null for anyone still holding a reference to this instance.
1390
+ // This is the core fix for issue #24.
1391
+ this.socketDir = null;
1392
+ if (!this.persistent) {
1393
+ this.databaseDir = null;
1394
+ }
1395
+ this.createdDatabases.clear();
1396
+ this._stopping = false;
1343
1397
  }
1344
1398
 
1345
1399
  /**
package/src/router.js CHANGED
@@ -14,6 +14,7 @@
14
14
  * PERFORMANCE: Uses Bun.listen() and Bun.connect() for 2-3x throughput improvement
15
15
  */
16
16
 
17
+ import fs from 'fs';
17
18
  import { PostgresManager } from './postgres.js';
18
19
  import { SyncManager } from './sync.js';
19
20
  import { RestoreManager } from './restore.js';
@@ -28,6 +29,14 @@ const SSL_REQUEST_CODE = 80877103;
28
29
  const GSSAPI_REQUEST_CODE = 80877104;
29
30
  const CANCEL_REQUEST_CODE = 80877102;
30
31
 
32
+ // Maximum size for the pre-handshake startup buffer. A legitimate PG
33
+ // startup message is at most a few hundred bytes; anything approaching
34
+ // 1 MiB is a runaway client or an attempted buffer-growth DoS. Bound
35
+ // this to stop the proxy from accumulating gigabytes of orphaned data
36
+ // when a client sends garbage and the handshake never completes.
37
+ // (Issue #18 root cause #2 — unbounded growth at state.buffer.)
38
+ const MAX_STARTUP_BUFFER_SIZE = 1024 * 1024; // 1 MiB
39
+
31
40
  /**
32
41
  * Attempt to write a pending buffer to a target socket.
33
42
  * Returns remaining unwritten bytes, or null if fully flushed.
@@ -231,6 +240,12 @@ export class MultiTenantRouter extends EventEmitter {
231
240
  pgSocket: null,
232
241
  dbName: null,
233
242
  handshakeComplete: false,
243
+ // startupInProgress serializes processStartupMessage() against async
244
+ // reentrancy — without it, every data event fired while the previous
245
+ // processStartupMessage() is still awaiting createDatabase() would
246
+ // launch another async task on the same state, racing to overwrite
247
+ // state.pgSocket and leaking the losers (issue #18 root cause #1).
248
+ startupInProgress: false,
234
249
  pendingToPg: null,
235
250
  pendingToClient: null
236
251
  });
@@ -249,9 +264,13 @@ export class MultiTenantRouter extends EventEmitter {
249
264
 
250
265
  // If handshake complete, forward to PostgreSQL
251
266
  if (state.handshakeComplete && state.pgSocket) {
252
- // If there's already pending data, append to it
267
+ // If there's already pending data, append and re-pause.
268
+ // (Re-pause is defensive: client should already be paused from the
269
+ // earlier partial-write, but kernel-buffered data can still arrive
270
+ // before the pause takes effect — issue #18 root cause #3.)
253
271
  if (state.pendingToPg) {
254
272
  state.pendingToPg = Buffer.concat([state.pendingToPg, data]);
273
+ socket.pause();
255
274
  return;
256
275
  }
257
276
  const written = state.pgSocket.write(data);
@@ -263,7 +282,20 @@ export class MultiTenantRouter extends EventEmitter {
263
282
  return;
264
283
  }
265
284
 
266
- // Buffer data for startup message parsing
285
+ // Buffer data for startup message parsing.
286
+ // Bound the pre-handshake buffer so a client that never completes its
287
+ // startup (or sends garbage) cannot grow state.buffer without limit —
288
+ // the 74 GiB VmSize in the production deadlock report traces to this
289
+ // path (issue #18 root cause #2).
290
+ const incomingSize = state.buffer ? state.buffer.length + data.byteLength : data.byteLength;
291
+ if (incomingSize > MAX_STARTUP_BUFFER_SIZE) {
292
+ this.logger.warn(
293
+ { incomingSize, limit: MAX_STARTUP_BUFFER_SIZE },
294
+ 'Pre-handshake buffer exceeded limit — closing connection'
295
+ );
296
+ socket.end();
297
+ return;
298
+ }
267
299
  if (state.buffer) {
268
300
  state.buffer = Buffer.concat([state.buffer, data]);
269
301
  } else {
@@ -275,9 +307,17 @@ export class MultiTenantRouter extends EventEmitter {
275
307
  }
276
308
 
277
309
  /**
278
- * Process PostgreSQL startup message and establish proxy connection
310
+ * Process PostgreSQL startup message and establish proxy connection.
311
+ *
312
+ * Guarded against async reentrancy: multiple data events arriving while
313
+ * the first processStartupMessage() is still awaiting createDatabase()
314
+ * or Bun.connect() must not launch concurrent tasks on the same state —
315
+ * they would race to assign state.pgSocket, leaking the losing sockets
316
+ * and double-writing the startup message (issue #18 root cause #1).
279
317
  */
280
318
  async processStartupMessage(socket, state) {
319
+ if (state.startupInProgress) return;
320
+
281
321
  const buffer = state.buffer;
282
322
  if (!buffer || buffer.length < 8) return; // Need at least length + protocol
283
323
 
@@ -315,6 +355,11 @@ export class MultiTenantRouter extends EventEmitter {
315
355
  const dbName = extractDatabaseName(startupMessage);
316
356
  state.dbName = dbName;
317
357
 
358
+ // Claim the reentrancy guard BEFORE the first await so subsequent data
359
+ // events (buffered into state.buffer by handleSocketData) cannot launch
360
+ // a second async task on the same state.
361
+ state.startupInProgress = true;
362
+
318
363
  try {
319
364
  // Auto-provision database if needed
320
365
  if (this.autoProvision) {
@@ -328,9 +373,13 @@ export class MultiTenantRouter extends EventEmitter {
328
373
  // Shared handler for pgSocket (used by both unix and TCP paths)
329
374
  const pgHandler = {
330
375
  data(_pgSocket, pgData) {
331
- // Forward PostgreSQL response to client with backpressure
376
+ // Forward PostgreSQL response to client with backpressure.
377
+ // Re-pause defensively when pendingToClient already exists —
378
+ // kernel-buffered PG data can arrive before the earlier pause()
379
+ // takes effect (issue #18 root cause #3).
332
380
  if (state.pendingToClient) {
333
381
  state.pendingToClient = Buffer.concat([state.pendingToClient, pgData]);
382
+ _pgSocket.pause();
334
383
  return;
335
384
  }
336
385
  const written = socket.write(pgData);
@@ -362,9 +411,18 @@ export class MultiTenantRouter extends EventEmitter {
362
411
  }
363
412
  };
364
413
 
365
- if (socketPath) {
414
+ // Safety net for issue #24: if socketPath points to a directory that was
415
+ // cleaned up (e.g. pgManager was stopped+started, or the PG subprocess
416
+ // exited unexpectedly and socketDir was reset to null but a stale cached
417
+ // path is still hanging around), fall back to TCP instead of Bun.connect
418
+ // hanging on a missing unix socket.
419
+ const useUnix = socketPath && fs.existsSync(socketPath);
420
+ if (useUnix) {
366
421
  state.pgSocket = await Bun.connect({ unix: socketPath, socket: pgHandler });
367
422
  } else {
423
+ if (socketPath && !useUnix) {
424
+ this.logger.warn({ socketPath, dbName }, 'Unix socket path stale — falling back to TCP');
425
+ }
368
426
  state.pgSocket = await Bun.connect({ hostname: '127.0.0.1', port: this.pgPort, socket: pgHandler });
369
427
  }
370
428
 
@@ -373,6 +431,13 @@ export class MultiTenantRouter extends EventEmitter {
373
431
  this.logger.error({ dbName, err: error }, 'Connection error');
374
432
  socket.end();
375
433
  this.emit('connection-error', { error, dbName });
434
+ } finally {
435
+ // Release the reentrancy guard whether handshake succeeded or not.
436
+ // If it succeeded, handshakeComplete is now true and further data
437
+ // events will bypass processStartupMessage anyway (handleSocketData
438
+ // takes the handshakeComplete path). If it failed, socket.end()
439
+ // has been called and the connection is tearing down.
440
+ state.startupInProgress = false;
376
441
  }
377
442
  }
378
443
 
@@ -161,6 +161,170 @@ test('Multi-tenant router - multiple databases isolated', async () => {
161
161
  cleanup();
162
162
  });
163
163
 
164
+ test('Router - pre-handshake buffer is bounded (issue #18 root cause #2)', async () => {
165
+ // Regression test for issue #18: without a bound on state.buffer, a
166
+ // client that sends garbage and never completes the PG startup would
167
+ // grow router memory unbounded (traced to the production 74 GiB VmSize).
168
+ // After fix, the router must close the connection once the buffer
169
+ // exceeds MAX_STARTUP_BUFFER_SIZE (1 MiB).
170
+ cleanup();
171
+
172
+ const router = await startMultiTenantServer({
173
+ port: 15546,
174
+ baseDir: testDataDir,
175
+ logLevel: 'warn',
176
+ });
177
+
178
+ const net = await import('net');
179
+ const sock = net.connect(15546, '127.0.0.1');
180
+ await new Promise((resolve) => sock.once('connect', resolve));
181
+
182
+ const garbage = Buffer.alloc(256 * 1024, 0x41); // 256 KiB of 'A'
183
+ let closed = false;
184
+ sock.on('close', () => { closed = true; });
185
+
186
+ // Send 5 × 256 KiB = 1.25 MiB, exceeding the 1 MiB cap.
187
+ for (let i = 0; i < 5 && !closed; i++) {
188
+ await new Promise((resolve) => sock.write(garbage, resolve));
189
+ }
190
+
191
+ // Wait up to 2s for the proxy to close the connection.
192
+ const deadline = Date.now() + 2000;
193
+ while (!closed && Date.now() < deadline) {
194
+ await new Promise((r) => setTimeout(r, 50));
195
+ }
196
+
197
+ expect(closed).toBe(true);
198
+ sock.destroy();
199
+
200
+ await router.stop();
201
+ cleanup();
202
+ });
203
+
204
+ test('Router - socket state has startupInProgress flag (issue #18 root cause #1)', async () => {
205
+ // White-box regression test for the reentrancy guard. Without
206
+ // state.startupInProgress, two data events arriving during the first
207
+ // processStartupMessage() await would launch concurrent async tasks
208
+ // that race to assign state.pgSocket, leaking the loser. This test
209
+ // verifies the flag is wired into the state object.
210
+ cleanup();
211
+
212
+ const router = await startMultiTenantServer({
213
+ port: 15547,
214
+ baseDir: testDataDir,
215
+ logLevel: 'warn',
216
+ });
217
+
218
+ const net = await import('net');
219
+ const sock = net.connect(15547, '127.0.0.1');
220
+ await new Promise((resolve) => sock.once('connect', resolve));
221
+
222
+ // Router tracks client sockets in this.connections; introspect to pull
223
+ // the state object and confirm the flag exists and defaults to false.
224
+ // On some platforms (macOS in particular) the client-side 'connect'
225
+ // event fires slightly before the server-side handleSocketOpen runs,
226
+ // so poll until the router has registered the connection rather than
227
+ // asserting synchronously.
228
+ const deadline = Date.now() + 2000;
229
+ while (router.connections.size === 0 && Date.now() < deadline) {
230
+ await new Promise((r) => setTimeout(r, 20));
231
+ }
232
+ expect(router.connections.size).toBeGreaterThan(0);
233
+ const bunSocket = [...router.connections][0];
234
+ const state = router.socketState.get(bunSocket);
235
+ expect(state).toBeDefined();
236
+ expect(state.startupInProgress).toBe(false);
237
+
238
+ sock.destroy();
239
+ await router.stop();
240
+ cleanup();
241
+ });
242
+
243
+ test('PostgresManager - stop() nulls socketDir/databaseDir (issue #24)', async () => {
244
+ // Regression test for issue #24: router used to cache stale socketPath
245
+ // pointing to a directory that stop() had already rmSync'd. After fix,
246
+ // stop() nulls socketDir/databaseDir UNCONDITIONALLY so subsequent
247
+ // getSocketPath() returns null (forcing TCP fallback in the router).
248
+ cleanup();
249
+
250
+ const { PostgresManager } = await import('../src/postgres.js');
251
+ const { createLogger } = await import('../src/logger.js');
252
+ const pg = new PostgresManager({
253
+ port: 15543,
254
+ logger: createLogger({ level: 'warn' }),
255
+ });
256
+
257
+ await pg.start();
258
+ const socketPathBeforeStop = pg.getSocketPath();
259
+ expect(socketPathBeforeStop).not.toBeNull();
260
+ expect(fs.existsSync(pg.socketDir)).toBe(true);
261
+
262
+ await pg.stop();
263
+
264
+ // CORE ASSERTION: socketDir must be nulled after stop
265
+ expect(pg.socketDir).toBeNull();
266
+ expect(pg.getSocketPath()).toBeNull();
267
+ // databaseDir nulled only in memory mode (persistent mode keeps user-owned path)
268
+ expect(pg.databaseDir).toBeNull();
269
+ // And the dir on disk must actually be gone
270
+ // (socketPathBeforeStop points inside the deleted socketDir)
271
+ const staleSocketDir = path.dirname(socketPathBeforeStop);
272
+ expect(fs.existsSync(staleSocketDir)).toBe(false);
273
+ });
274
+
275
+ test('PostgresManager - start()+stop()+start() yields fresh socketDir (issue #24)', async () => {
276
+ // Regression test for issue #24: pgManager.start() called after stop()
277
+ // must produce a FRESH socketDir (different path). Without the fix, a
278
+ // re-entry guard was missing and socketDir could leak across restarts.
279
+ cleanup();
280
+
281
+ const { PostgresManager } = await import('../src/postgres.js');
282
+ const { createLogger } = await import('../src/logger.js');
283
+ const pg = new PostgresManager({
284
+ port: 15544,
285
+ logger: createLogger({ level: 'warn' }),
286
+ });
287
+
288
+ await pg.start();
289
+ const socketDir1 = pg.socketDir;
290
+ expect(socketDir1).not.toBeNull();
291
+
292
+ await pg.stop();
293
+ expect(pg.socketDir).toBeNull();
294
+
295
+ await pg.start();
296
+ const socketDir2 = pg.socketDir;
297
+ expect(socketDir2).not.toBeNull();
298
+ expect(socketDir2).not.toBe(socketDir1);
299
+ expect(fs.existsSync(socketDir2)).toBe(true);
300
+
301
+ await pg.stop();
302
+ });
303
+
304
+ test('PostgresManager - double start() is a no-op (issue #24 re-entry guard)', async () => {
305
+ // Without the guard, a second start() would overwrite socketDir/databaseDir
306
+ // and leak the previous tmp dir (the "1,457 stale sock dirs" symptom).
307
+ cleanup();
308
+
309
+ const { PostgresManager } = await import('../src/postgres.js');
310
+ const { createLogger } = await import('../src/logger.js');
311
+ const pg = new PostgresManager({
312
+ port: 15545,
313
+ logger: createLogger({ level: 'warn' }),
314
+ });
315
+
316
+ await pg.start();
317
+ const socketDir1 = pg.socketDir;
318
+
319
+ // Second start() should silently return the same instance without
320
+ // reassigning socketDir/databaseDir.
321
+ const result = await pg.start();
322
+ expect(result).toBe(pg);
323
+ expect(pg.socketDir).toBe(socketDir1);
324
+
325
+ await pg.stop();
326
+ });
327
+
164
328
  test('Multi-tenant router - instance reuse', async () => {
165
329
  cleanup();
166
330
 
@@ -1,30 +0,0 @@
1
- # GitHub Release Notes Configuration
2
- # Automatically generates release notes from merged PRs
3
- # See: https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes
4
-
5
- changelog:
6
- exclude:
7
- labels:
8
- - ignore-for-changelog
9
- - internal
10
- - bot
11
- authors:
12
- - github-actions[bot]
13
- categories:
14
- - title: "Features"
15
- labels:
16
- - feature
17
- - enhancement
18
- - title: "Bug Fixes"
19
- labels:
20
- - bug
21
- - fix
22
- - title: "Breaking Changes"
23
- labels:
24
- - breaking
25
- - title: "Dependencies"
26
- labels:
27
- - dependencies
28
- - title: "Other Changes"
29
- labels:
30
- - "*"