@volter/world-runtime 3.0.46 → 3.0.48

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.
@@ -2,6 +2,11 @@ export interface InfraService {
2
2
  kind: string;
3
3
  hostPort: number;
4
4
  replicaSet?: string;
5
+ postgres?: {
6
+ user: string;
7
+ database: string;
8
+ password?: string;
9
+ };
5
10
  }
6
11
  /** Read the generated definition's loopback port mappings. MongoDB may explicitly request
7
12
  * one replica-set member through its Compose-compatible command; other mongod options are refused. */
@@ -44,7 +44,20 @@ export function parseInfraDefinition(text) {
44
44
  });
45
45
  if (ports.length !== 1)
46
46
  throw new Error(`managed infrastructure ${kind} needs exactly one declared loopback port`);
47
- services.push({ kind, hostPort: ports[0], ...(replicaSet ? { replicaSet } : {}) });
47
+ let postgres;
48
+ if (kind === 'postgres') {
49
+ const environment = declared.environment ?? {};
50
+ const user = environment.POSTGRES_USER ?? 'postgres';
51
+ const database = environment.POSTGRES_DB ?? user;
52
+ const password = environment.POSTGRES_PASSWORD;
53
+ if ([user, database, password].some(value => value !== undefined && (typeof value !== 'string' || !value || /[\r\n\0]|\$\{/.test(value)))) {
54
+ throw new Error('World-managed PGlite requires literal POSTGRES_USER, POSTGRES_DB and POSTGRES_PASSWORD');
55
+ }
56
+ if ([user, database].some(value => Buffer.byteLength(value, 'utf8') > 63))
57
+ throw new Error('World-managed PGlite identifiers must fit PostgreSQL\'s 63-byte name limit');
58
+ postgres = { user: user, database: database, ...(password !== undefined ? { password: password } : {}) };
59
+ }
60
+ services.push({ kind, hostPort: ports[0], ...(replicaSet ? { replicaSet } : {}), ...(postgres ? { postgres } : {}) });
48
61
  }
49
62
  return services;
50
63
  }
@@ -160,6 +173,8 @@ export async function pgliteUp(services, dataDir, launch = {}) {
160
173
  }
161
174
  const name = CONTAINERLESS[service.kind] ?? `pglite-${service.kind}`;
162
175
  const serviceData = join(dataDir, containerlessDataDirectory(service.kind) ?? `${name}-data`);
176
+ if (service.kind === 'postgres' && !service.postgres?.password)
177
+ throw new Error('World-managed PGlite requires a literal POSTGRES_PASSWORD; declare a throwaway World credential');
163
178
  const log = join(dataDir, `${name}.log`);
164
179
  if (await listening(service.hostPort)) {
165
180
  throw new Error(`${name} cannot serve port ${service.hostPort}: another process is already listening on it`);
@@ -177,7 +192,8 @@ export async function pgliteUp(services, dataDir, launch = {}) {
177
192
  const logFd = openSync(log, 'a');
178
193
  const child = spawn(command, args, {
179
194
  windowsHide: true,
180
- env,
195
+ env: service.postgres ? { ...env, VOLTER_WORLD_PGLITE_USER: service.postgres.user,
196
+ VOLTER_WORLD_PGLITE_DATABASE: service.postgres.database, VOLTER_WORLD_PGLITE_PASSWORD: service.postgres.password } : env,
181
197
  detached: true,
182
198
  stdio: ['ignore', logFd, logFd],
183
199
  });
@@ -33,18 +33,11 @@
33
33
  //
34
34
  // Limits, stated plainly (measured on 0.5.8 through this gateway with `pg`
35
35
  // clients; pglite-backing.test.ts holds the extension and database checks):
36
- // - ONE database. Trust auth accepts any user/password/database in the
37
- // startup packet, so a DATABASE_URL naming `/rallly` connects — but every
38
- // name lands in PGlite's single `postgres` database: current_database()
39
- // and current_user answer `postgres`, and all names share one catalog.
40
- // PGlite itself ACCEPTS `CREATE DATABASE` (a pg_database row appears) yet
41
- // nothing can ever connect to it, so this host fails the statement loudly
42
- // (see refusal() below): 42P04 "already exists" when the name is the
43
- // connection's own startup database (true: it connects, so check-then-
44
- // create for the app's own database proceeds), 0A000 "not supported" for
45
- // any other name. An app that needs a second real database (Prisma's
46
- // shadow database for `migrate dev`, Django's `test_<name>`; `migrate
47
- // deploy` needs none) needs the container backing.
36
+ // - ONE declared database and login, authenticated with the World's throwaway
37
+ // password. Undeclared databases fail 3D000; failed logins fail 28P01.
38
+ // current_database() and current_user come from the configured backend.
39
+ // CREATE DATABASE of the declared name fails 42P04, other names fail 0A000;
40
+ // multiple databases require the native or container backing.
48
41
  // - No COPY … FROM STDIN: refused with 0A000 (it aborts PGlite's backend);
49
42
  // COPY … TO STDOUT works.
50
43
  // - ONE serialized session. Connections are accepted concurrently but run
@@ -96,6 +89,7 @@ import net from 'node:net';
96
89
  import { mkdirSync, readFileSync } from 'node:fs';
97
90
  import clockForm from '@volter/world-core/world-clock';
98
91
  import { fromNodeSocket } from 'pg-gateway/node';
92
+ import { BackendError } from 'pg-gateway';
99
93
 
100
94
  // ---- the World's time (above) --------------------------------
101
95
  const machineNow = Date.now.bind(Date);
@@ -147,6 +141,13 @@ function arg(name) {
147
141
 
148
142
  const port = Number(arg('--port'));
149
143
  const dataDir = arg('--data');
144
+ const databaseUser = process.env.VOLTER_WORLD_PGLITE_USER;
145
+ const databaseName = process.env.VOLTER_WORLD_PGLITE_DATABASE;
146
+ const databasePassword = process.env.VOLTER_WORLD_PGLITE_PASSWORD;
147
+ if (![databaseUser, databaseName, databasePassword].every(value => typeof value === 'string' && value.length > 0)) {
148
+ process.stderr.write('pglite-host: declared database identity and throwaway password are required\n');
149
+ process.exit(2);
150
+ }
150
151
  if (!Number.isInteger(port) || port < 1 || port > 65535) {
151
152
  process.stderr.write('pglite-host: --port must be 1-65535\n');
152
153
  process.exit(2);
@@ -159,7 +160,27 @@ process.stderr.on('error', () => {});
159
160
  // PGlite's create() only progresses while something holds the event loop;
160
161
  // listening starts AFTER readiness so "port answers" means "serves queries"
161
162
  const initKeepAlive = setInterval(() => {}, 500);
162
- const dbReady = PGlite.create({ dataDir, extensions });
163
+ const quoteIdentifier = value => `"${value.replaceAll('"', '""')}"`;
164
+ async function openDeclaredDatabase() {
165
+ // PostgreSQL cannot rename its current database. Use its maintenance database
166
+ // before opening the declared one; old Worlds' rows in postgres stay intact.
167
+ const maintenance = await PGlite.create({ dataDir, extensions, database: 'template1' });
168
+ try {
169
+ const existing = await maintenance.query('SELECT datname FROM pg_database WHERE datname = $1', [databaseName]);
170
+ if (!existing.rows.length) {
171
+ const legacy = await maintenance.query("SELECT datname FROM pg_database WHERE datname = 'postgres'");
172
+ await maintenance.exec(legacy.rows.length
173
+ ? `ALTER DATABASE postgres RENAME TO ${quoteIdentifier(databaseName)}`
174
+ : `CREATE DATABASE ${quoteIdentifier(databaseName)}`);
175
+ }
176
+ const role = await maintenance.query('SELECT rolname FROM pg_roles WHERE rolname = $1', [databaseUser]);
177
+ if (!role.rows.length) await maintenance.exec(`CREATE ROLE ${quoteIdentifier(databaseUser)} LOGIN SUPERUSER`);
178
+ } finally { await maintenance.close(); }
179
+ const db = await PGlite.create({ dataDir, extensions, database: databaseName });
180
+ await db.exec(`SET SESSION AUTHORIZATION ${quoteIdentifier(databaseUser)}`);
181
+ return db;
182
+ }
183
+ const dbReady = openDeclaredDatabase();
163
184
  dbReady.catch((error) => {
164
185
  process.stderr.write(`pglite-host: backend failed to start: ${errorMessage(error)}\n`);
165
186
  process.exit(1);
@@ -624,17 +645,9 @@ function lastReadyStatus(response) {
624
645
  }
625
646
 
626
647
  // ---- CREATE DATABASE refusal -----------------------------------------------
627
- // PGlite accepts CREATE DATABASE (a pg_database row appears) but nothing can
628
- // ever connect to it: every startup database name aliases to `postgres`. A
629
- // client that "creates" a database and then connects to it would silently land
630
- // in the one real database (a Prisma shadow database would replay migrations
631
- // into it; a test runner would treat it as disposable). So the statement fails:
632
- // - naming the connection's OWN startup database (or `postgres`), with 42P04
633
- // duplicate_database — true, since that name connects, and the answer
634
- // check-then-create setup (`rails db:create`) tolerates;
635
- // - naming any other database, with 0A000 feature_not_supported, because a
636
- // tool that treats 42P04 as "reuse it" would then connect to the alias of
637
- // the real database and treat it as disposable.
648
+ // Only the declared database can be connected to. Creating another catalog
649
+ // row would advertise a database this service cannot serve, so the statement
650
+ // fails: 42P04 for the declared name, 0A000 for another name.
638
651
  // The statement text is rewritten to a DO block that raises, so the BACKEND
639
652
  // reports the error and owns the protocol and transaction state
640
653
  // (ErrorResponse, aborted block, ReadyForQuery) as for any failing statement.
@@ -662,9 +675,9 @@ const raising = (errcode, message, detail, hint) => `DO ${DOLLAR_TAG} BEGIN RAIS
662
675
  * `own`, in plain SQL literal quoting. */
663
676
  function refusal(name, own) {
664
677
  const label = name && !name.includes(DOLLAR_TAG) ? ` "${name}"` : '';
665
- const exists = name !== undefined && (name === own || name === 'postgres');
666
- const detail = "The World's PGlite backing serves ONE database; every database name a client connects with is an alias for it, so CREATE DATABASE cannot make a separate one.";
667
- const hint = 'Use the database named in DATABASE_URL, or run the World with a container runtime for multiple databases.';
678
+ const exists = name !== undefined && name === own;
679
+ const detail = "The World's PGlite backing serves ONE database; only its declared database can be connected to, so CREATE DATABASE cannot make a separately served one.";
680
+ const hint = 'Use the database named in DATABASE_URL, or select the native PostgreSQL or container backing for multiple databases.';
668
681
  return exists
669
682
  ? raising('duplicate_database', `database${label} already exists`, detail, hint)
670
683
  : raising('feature_not_supported', `CREATE DATABASE${label} is not supported: this World serves one database`, detail, hint);
@@ -785,7 +798,12 @@ const server = net.createServer(async (socket) => {
785
798
  socket.on('error', () => { /* close handler owns cleanup */ });
786
799
  await fromNodeSocket(socket, {
787
800
  serverVersion: '18.3 (PGlite)',
788
- auth: { method: 'trust' },
801
+ auth: { method: 'password', getClearTextPassword: () => databasePassword,
802
+ validateCredentials: ({ username, password }) => username === databaseUser && password === databasePassword },
803
+ onStartup({ clientParams }) {
804
+ const requested = clientParams?.database || clientParams?.user;
805
+ if (requested !== databaseName) throw BackendError.create({ severity: 'FATAL', code: '3D000', message: `database "${requested}" does not exist` });
806
+ },
789
807
  async onMessage(data, { isAuthenticated, clientParams }) {
790
808
  if (!isAuthenticated) return undefined; // gateway owns startup/auth traffic
791
809
  const client = data;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/world-runtime",
3
- "version": "3.0.46",
3
+ "version": "3.0.48",
4
4
  "description": "World configs for twins: boot named local runtimes, allocate ports, generate world.env/instance.json, and run apps against fake-key twin worlds.",
5
5
  "keywords": [
6
6
  "twin",
@@ -69,14 +69,14 @@
69
69
  "node": ">=22.3"
70
70
  },
71
71
  "peerDependencies": {
72
- "@volter/world-core": "3.0.46",
73
- "@volter/world-console": "3.0.41"
72
+ "@volter/world-core": "3.0.48",
73
+ "@volter/world-console": "3.0.43"
74
74
  },
75
75
  "dependencies": {
76
76
  "@electric-sql/pglite": "0.5.8",
77
77
  "@electric-sql/pglite-pgvector": "0.0.9",
78
78
  "@volter/world-access": "3.0.1",
79
- "@volter/world-core": "3.0.46",
79
+ "@volter/world-core": "3.0.48",
80
80
  "mongodb": "~6.20.0",
81
81
  "pg-gateway": "0.3.0-beta.4",
82
82
  "smol-toml": "^1.8.0",
@@ -23,7 +23,7 @@ import { fileURLToPath } from 'node:url';
23
23
  import { parse } from 'yaml';
24
24
  import { mongodbArgv, mongodbListening, mongodbReady } from './mongodb-backing.ts';
25
25
 
26
- export interface InfraService { kind: string; hostPort: number; replicaSet?: string }
26
+ export interface InfraService { kind: string; hostPort: number; replicaSet?: string; postgres?: { user: string; database: string; password?: string } }
27
27
 
28
28
  /** Read the generated definition's loopback port mappings. MongoDB may explicitly request
29
29
  * one replica-set member through its Compose-compatible command; other mongod options are refused. */
@@ -31,7 +31,7 @@ export function parseInfraDefinition(text: string): InfraService[] {
31
31
  const definition = parse(text);
32
32
  const services: InfraService[] = [];
33
33
  for (const [kind, value] of Object.entries(definition?.services ?? {})) {
34
- const declared = value as { ports?: unknown[]; command?: unknown };
34
+ const declared = value as { ports?: unknown[]; command?: unknown; environment?: Record<string, unknown> };
35
35
  let replicaSet: string | undefined;
36
36
  if (kind === 'mongodb' && declared.command !== undefined) {
37
37
  const command = declared.command;
@@ -46,7 +46,19 @@ export function parseInfraDefinition(text: string): InfraService[] {
46
46
  return match ? [Number(match[1])] : [];
47
47
  });
48
48
  if (ports.length !== 1) throw new Error(`managed infrastructure ${kind} needs exactly one declared loopback port`);
49
- services.push({ kind, hostPort: ports[0]!, ...(replicaSet ? { replicaSet } : {}) });
49
+ let postgres: InfraService['postgres'];
50
+ if (kind === 'postgres') {
51
+ const environment = declared.environment ?? {};
52
+ const user = environment.POSTGRES_USER ?? 'postgres';
53
+ const database = environment.POSTGRES_DB ?? user;
54
+ const password = environment.POSTGRES_PASSWORD;
55
+ if ([user, database, password].some(value => value !== undefined && (typeof value !== 'string' || !value || /[\r\n\0]|\$\{/.test(value)))) {
56
+ throw new Error('World-managed PGlite requires literal POSTGRES_USER, POSTGRES_DB and POSTGRES_PASSWORD');
57
+ }
58
+ if ([user, database].some(value => Buffer.byteLength(value as string, 'utf8') > 63)) throw new Error('World-managed PGlite identifiers must fit PostgreSQL\'s 63-byte name limit');
59
+ postgres = { user: user as string, database: database as string, ...(password !== undefined ? { password: password as string } : {}) };
60
+ }
61
+ services.push({ kind, hostPort: ports[0]!, ...(replicaSet ? { replicaSet } : {}), ...(postgres ? { postgres } : {}) });
50
62
  }
51
63
  return services;
52
64
  }
@@ -167,6 +179,7 @@ export async function pgliteUp(services: InfraService[], dataDir: string, launch
167
179
  }
168
180
  const name = CONTAINERLESS[service.kind] ?? `pglite-${service.kind}`;
169
181
  const serviceData = join(dataDir, containerlessDataDirectory(service.kind) ?? `${name}-data`);
182
+ if (service.kind === 'postgres' && !service.postgres?.password) throw new Error('World-managed PGlite requires a literal POSTGRES_PASSWORD; declare a throwaway World credential');
170
183
  const log = join(dataDir, `${name}.log`);
171
184
  if (await listening(service.hostPort)) {
172
185
  throw new Error(`${name} cannot serve port ${service.hostPort}: another process is already listening on it`);
@@ -184,7 +197,8 @@ export async function pgliteUp(services: InfraService[], dataDir: string, launch
184
197
  const logFd = openSync(log, 'a');
185
198
  const child = spawn(command!, args, {
186
199
  windowsHide: true,
187
- env,
200
+ env: service.postgres ? { ...env, VOLTER_WORLD_PGLITE_USER: service.postgres.user,
201
+ VOLTER_WORLD_PGLITE_DATABASE: service.postgres.database, VOLTER_WORLD_PGLITE_PASSWORD: service.postgres.password } : env,
188
202
  detached: true,
189
203
  stdio: ['ignore', logFd, logFd],
190
204
  });
@@ -33,18 +33,11 @@
33
33
  //
34
34
  // Limits, stated plainly (measured on 0.5.8 through this gateway with `pg`
35
35
  // clients; pglite-backing.test.ts holds the extension and database checks):
36
- // - ONE database. Trust auth accepts any user/password/database in the
37
- // startup packet, so a DATABASE_URL naming `/rallly` connects — but every
38
- // name lands in PGlite's single `postgres` database: current_database()
39
- // and current_user answer `postgres`, and all names share one catalog.
40
- // PGlite itself ACCEPTS `CREATE DATABASE` (a pg_database row appears) yet
41
- // nothing can ever connect to it, so this host fails the statement loudly
42
- // (see refusal() below): 42P04 "already exists" when the name is the
43
- // connection's own startup database (true: it connects, so check-then-
44
- // create for the app's own database proceeds), 0A000 "not supported" for
45
- // any other name. An app that needs a second real database (Prisma's
46
- // shadow database for `migrate dev`, Django's `test_<name>`; `migrate
47
- // deploy` needs none) needs the container backing.
36
+ // - ONE declared database and login, authenticated with the World's throwaway
37
+ // password. Undeclared databases fail 3D000; failed logins fail 28P01.
38
+ // current_database() and current_user come from the configured backend.
39
+ // CREATE DATABASE of the declared name fails 42P04, other names fail 0A000;
40
+ // multiple databases require the native or container backing.
48
41
  // - No COPY … FROM STDIN: refused with 0A000 (it aborts PGlite's backend);
49
42
  // COPY … TO STDOUT works.
50
43
  // - ONE serialized session. Connections are accepted concurrently but run
@@ -96,6 +89,7 @@ import net from 'node:net';
96
89
  import { mkdirSync, readFileSync } from 'node:fs';
97
90
  import clockForm from '@volter/world-core/world-clock';
98
91
  import { fromNodeSocket } from 'pg-gateway/node';
92
+ import { BackendError } from 'pg-gateway';
99
93
 
100
94
  // ---- the World's time (above) --------------------------------
101
95
  const machineNow = Date.now.bind(Date);
@@ -147,6 +141,13 @@ function arg(name) {
147
141
 
148
142
  const port = Number(arg('--port'));
149
143
  const dataDir = arg('--data');
144
+ const databaseUser = process.env.VOLTER_WORLD_PGLITE_USER;
145
+ const databaseName = process.env.VOLTER_WORLD_PGLITE_DATABASE;
146
+ const databasePassword = process.env.VOLTER_WORLD_PGLITE_PASSWORD;
147
+ if (![databaseUser, databaseName, databasePassword].every(value => typeof value === 'string' && value.length > 0)) {
148
+ process.stderr.write('pglite-host: declared database identity and throwaway password are required\n');
149
+ process.exit(2);
150
+ }
150
151
  if (!Number.isInteger(port) || port < 1 || port > 65535) {
151
152
  process.stderr.write('pglite-host: --port must be 1-65535\n');
152
153
  process.exit(2);
@@ -159,7 +160,27 @@ process.stderr.on('error', () => {});
159
160
  // PGlite's create() only progresses while something holds the event loop;
160
161
  // listening starts AFTER readiness so "port answers" means "serves queries"
161
162
  const initKeepAlive = setInterval(() => {}, 500);
162
- const dbReady = PGlite.create({ dataDir, extensions });
163
+ const quoteIdentifier = value => `"${value.replaceAll('"', '""')}"`;
164
+ async function openDeclaredDatabase() {
165
+ // PostgreSQL cannot rename its current database. Use its maintenance database
166
+ // before opening the declared one; old Worlds' rows in postgres stay intact.
167
+ const maintenance = await PGlite.create({ dataDir, extensions, database: 'template1' });
168
+ try {
169
+ const existing = await maintenance.query('SELECT datname FROM pg_database WHERE datname = $1', [databaseName]);
170
+ if (!existing.rows.length) {
171
+ const legacy = await maintenance.query("SELECT datname FROM pg_database WHERE datname = 'postgres'");
172
+ await maintenance.exec(legacy.rows.length
173
+ ? `ALTER DATABASE postgres RENAME TO ${quoteIdentifier(databaseName)}`
174
+ : `CREATE DATABASE ${quoteIdentifier(databaseName)}`);
175
+ }
176
+ const role = await maintenance.query('SELECT rolname FROM pg_roles WHERE rolname = $1', [databaseUser]);
177
+ if (!role.rows.length) await maintenance.exec(`CREATE ROLE ${quoteIdentifier(databaseUser)} LOGIN SUPERUSER`);
178
+ } finally { await maintenance.close(); }
179
+ const db = await PGlite.create({ dataDir, extensions, database: databaseName });
180
+ await db.exec(`SET SESSION AUTHORIZATION ${quoteIdentifier(databaseUser)}`);
181
+ return db;
182
+ }
183
+ const dbReady = openDeclaredDatabase();
163
184
  dbReady.catch((error) => {
164
185
  process.stderr.write(`pglite-host: backend failed to start: ${errorMessage(error)}\n`);
165
186
  process.exit(1);
@@ -624,17 +645,9 @@ function lastReadyStatus(response) {
624
645
  }
625
646
 
626
647
  // ---- CREATE DATABASE refusal -----------------------------------------------
627
- // PGlite accepts CREATE DATABASE (a pg_database row appears) but nothing can
628
- // ever connect to it: every startup database name aliases to `postgres`. A
629
- // client that "creates" a database and then connects to it would silently land
630
- // in the one real database (a Prisma shadow database would replay migrations
631
- // into it; a test runner would treat it as disposable). So the statement fails:
632
- // - naming the connection's OWN startup database (or `postgres`), with 42P04
633
- // duplicate_database — true, since that name connects, and the answer
634
- // check-then-create setup (`rails db:create`) tolerates;
635
- // - naming any other database, with 0A000 feature_not_supported, because a
636
- // tool that treats 42P04 as "reuse it" would then connect to the alias of
637
- // the real database and treat it as disposable.
648
+ // Only the declared database can be connected to. Creating another catalog
649
+ // row would advertise a database this service cannot serve, so the statement
650
+ // fails: 42P04 for the declared name, 0A000 for another name.
638
651
  // The statement text is rewritten to a DO block that raises, so the BACKEND
639
652
  // reports the error and owns the protocol and transaction state
640
653
  // (ErrorResponse, aborted block, ReadyForQuery) as for any failing statement.
@@ -662,9 +675,9 @@ const raising = (errcode, message, detail, hint) => `DO ${DOLLAR_TAG} BEGIN RAIS
662
675
  * `own`, in plain SQL literal quoting. */
663
676
  function refusal(name, own) {
664
677
  const label = name && !name.includes(DOLLAR_TAG) ? ` "${name}"` : '';
665
- const exists = name !== undefined && (name === own || name === 'postgres');
666
- const detail = "The World's PGlite backing serves ONE database; every database name a client connects with is an alias for it, so CREATE DATABASE cannot make a separate one.";
667
- const hint = 'Use the database named in DATABASE_URL, or run the World with a container runtime for multiple databases.';
678
+ const exists = name !== undefined && name === own;
679
+ const detail = "The World's PGlite backing serves ONE database; only its declared database can be connected to, so CREATE DATABASE cannot make a separately served one.";
680
+ const hint = 'Use the database named in DATABASE_URL, or select the native PostgreSQL or container backing for multiple databases.';
668
681
  return exists
669
682
  ? raising('duplicate_database', `database${label} already exists`, detail, hint)
670
683
  : raising('feature_not_supported', `CREATE DATABASE${label} is not supported: this World serves one database`, detail, hint);
@@ -785,7 +798,12 @@ const server = net.createServer(async (socket) => {
785
798
  socket.on('error', () => { /* close handler owns cleanup */ });
786
799
  await fromNodeSocket(socket, {
787
800
  serverVersion: '18.3 (PGlite)',
788
- auth: { method: 'trust' },
801
+ auth: { method: 'password', getClearTextPassword: () => databasePassword,
802
+ validateCredentials: ({ username, password }) => username === databaseUser && password === databasePassword },
803
+ onStartup({ clientParams }) {
804
+ const requested = clientParams?.database || clientParams?.user;
805
+ if (requested !== databaseName) throw BackendError.create({ severity: 'FATAL', code: '3D000', message: `database "${requested}" does not exist` });
806
+ },
789
807
  async onMessage(data, { isAuthenticated, clientParams }) {
790
808
  if (!isAuthenticated) return undefined; // gateway owns startup/auth traffic
791
809
  const client = data;