postgresai 0.16.0-rc.4 → 0.16.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.
Files changed (85) hide show
  1. package/README.md +154 -0
  2. package/dist/bin/postgres-ai.js +2911 -255
  3. package/package.json +12 -3
  4. package/schemas/A002.schema.json +63 -0
  5. package/schemas/A003.schema.json +73 -0
  6. package/schemas/A004.schema.json +81 -0
  7. package/schemas/A007.schema.json +71 -0
  8. package/schemas/A013.schema.json +61 -0
  9. package/schemas/D001.schema.json +71 -0
  10. package/schemas/D004.schema.json +136 -0
  11. package/schemas/F001.schema.json +73 -0
  12. package/schemas/F002.schema.json +108 -0
  13. package/schemas/F003.schema.json +138 -0
  14. package/schemas/F004.schema.json +125 -0
  15. package/schemas/F005.schema.json +131 -0
  16. package/schemas/F009.schema.json +155 -0
  17. package/schemas/G001.schema.json +135 -0
  18. package/schemas/G003.schema.json +90 -0
  19. package/schemas/H001.schema.json +141 -0
  20. package/schemas/H002.schema.json +129 -0
  21. package/schemas/H004.schema.json +128 -0
  22. package/schemas/I001.schema.json +149 -0
  23. package/schemas/K001.schema.json +161 -0
  24. package/schemas/K003.schema.json +163 -0
  25. package/schemas/K004.schema.json +110 -0
  26. package/schemas/K005.schema.json +110 -0
  27. package/schemas/K006.schema.json +110 -0
  28. package/schemas/K007.schema.json +110 -0
  29. package/schemas/K008.schema.json +110 -0
  30. package/schemas/M001.schema.json +119 -0
  31. package/schemas/M002.schema.json +110 -0
  32. package/schemas/M003.schema.json +128 -0
  33. package/schemas/N001.schema.json +161 -0
  34. package/schemas/query.schema.json +62 -0
  35. package/CHANGELOG.md +0 -11
  36. package/bin/postgres-ai.ts +0 -5578
  37. package/bun.lock +0 -258
  38. package/bunfig.toml +0 -20
  39. package/lib/aas-onboard.ts +0 -251
  40. package/lib/auth-server.ts +0 -285
  41. package/lib/checkup-api.ts +0 -526
  42. package/lib/checkup-dictionary.ts +0 -103
  43. package/lib/checkup-summary.ts +0 -338
  44. package/lib/checkup.ts +0 -2261
  45. package/lib/config.ts +0 -171
  46. package/lib/init.ts +0 -1152
  47. package/lib/instances.ts +0 -245
  48. package/lib/issues.ts +0 -1060
  49. package/lib/mcp-server.ts +0 -667
  50. package/lib/metrics-loader.ts +0 -134
  51. package/lib/pkce.ts +0 -79
  52. package/lib/reports.ts +0 -373
  53. package/lib/storage.ts +0 -367
  54. package/lib/supabase.ts +0 -826
  55. package/lib/util.ts +0 -134
  56. package/packages/postgres-ai/README.md +0 -26
  57. package/packages/postgres-ai/bin/postgres-ai.js +0 -27
  58. package/packages/postgres-ai/package.json +0 -27
  59. package/scripts/embed-checkup-dictionary.ts +0 -115
  60. package/scripts/embed-metrics.ts +0 -160
  61. package/scripts/generate-release-notes.ts +0 -668
  62. package/test/PERMISSION_CHECK_TEST_SUMMARY.md +0 -139
  63. package/test/aas-onboard.test.ts +0 -301
  64. package/test/auth.test.ts +0 -287
  65. package/test/checkup.integration.test.ts +0 -413
  66. package/test/checkup.test.ts +0 -3626
  67. package/test/compose-cmd.test.ts +0 -120
  68. package/test/config-consistency.test.ts +0 -352
  69. package/test/init.integration.test.ts +0 -438
  70. package/test/init.test.ts +0 -1816
  71. package/test/issues.cli.test.ts +0 -1162
  72. package/test/issues.test.ts +0 -456
  73. package/test/mcp-server.test.ts +0 -2530
  74. package/test/monitoring.test.ts +0 -746
  75. package/test/permission-check-sql.test.ts +0 -116
  76. package/test/reports.cli.test.ts +0 -793
  77. package/test/reports.test.ts +0 -977
  78. package/test/schema-validation.test.ts +0 -231
  79. package/test/storage.test.ts +0 -935
  80. package/test/supabase.test.ts +0 -709
  81. package/test/targets-add-config.test.ts +0 -28
  82. package/test/test-utils.ts +0 -190
  83. package/test/upgrade.test.ts +0 -1056
  84. package/test/util.test.ts +0 -44
  85. package/tsconfig.json +0 -20
@@ -1,438 +0,0 @@
1
- /**
2
- * Integration tests for prepare-db command
3
- * Requires PostgreSQL binaries (initdb, postgres) to be available
4
- * These tests spin up a temporary PostgreSQL instance for realistic testing
5
- */
6
- import { describe, test, expect, afterAll } from "bun:test";
7
- import * as fs from "fs";
8
- import * as os from "os";
9
- import * as path from "path";
10
- import * as net from "net";
11
- import { Client } from "pg";
12
-
13
- const TEST_TIMEOUT = 30000; // 30 seconds
14
-
15
- function sqlLiteral(value: string): string {
16
- return `'${String(value).replace(/'/g, "''")}'`;
17
- }
18
-
19
- function findOnPath(cmd: string): string | null {
20
- const result = Bun.spawnSync(["sh", "-c", `command -v ${cmd}`]);
21
- if (result.exitCode === 0) {
22
- return new TextDecoder().decode(result.stdout).trim();
23
- }
24
- return null;
25
- }
26
-
27
- function findPgBin(cmd: string): string | null {
28
- const p = findOnPath(cmd);
29
- if (p) return p;
30
-
31
- // Debian/Ubuntu (GitLab CI node:*-bullseye images): binaries usually live here.
32
- const probe = Bun.spawnSync([
33
- "sh",
34
- "-c",
35
- `ls -1 /usr/lib/postgresql/*/bin/${cmd} 2>/dev/null | head -n 1 || true`,
36
- ]);
37
- const out = new TextDecoder().decode(probe.stdout).trim();
38
- if (out) return out;
39
-
40
- return null;
41
- }
42
-
43
- function havePostgresBinaries(): boolean {
44
- return !!(findPgBin("initdb") && findPgBin("postgres"));
45
- }
46
-
47
- function isRunningAsRoot(): boolean {
48
- return process.getuid?.() === 0;
49
- }
50
-
51
- async function getFreePort(): Promise<number> {
52
- return new Promise((resolve, reject) => {
53
- const srv = net.createServer();
54
- srv.listen(0, "127.0.0.1", () => {
55
- const addr = srv.address() as net.AddressInfo;
56
- srv.close((err) => {
57
- if (err) return reject(err);
58
- resolve(addr.port);
59
- });
60
- });
61
- srv.on("error", reject);
62
- });
63
- }
64
-
65
- async function waitFor<T>(
66
- fn: () => Promise<T>,
67
- { timeoutMs = 10000, intervalMs = 100 } = {}
68
- ): Promise<T> {
69
- const start = Date.now();
70
- while (true) {
71
- try {
72
- return await fn();
73
- } catch (e) {
74
- if (Date.now() - start > timeoutMs) throw e;
75
- await new Promise((r) => setTimeout(r, intervalMs));
76
- }
77
- }
78
- }
79
-
80
- interface TempPostgres {
81
- port: number;
82
- socketDir: string;
83
- adminUri: string;
84
- postgresPassword: string;
85
- cleanup: () => Promise<void>;
86
- }
87
-
88
- async function createTempPostgres(): Promise<TempPostgres> {
89
- const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "postgresai-init-"));
90
- const dataDir = path.join(tmpRoot, "data");
91
- const socketDir = path.join(tmpRoot, "sock");
92
- fs.mkdirSync(socketDir, { recursive: true });
93
-
94
- const initdb = findPgBin("initdb");
95
- const postgresBin = findPgBin("postgres");
96
- if (!initdb || !postgresBin) {
97
- throw new Error("PostgreSQL binaries not found (need initdb and postgres)");
98
- }
99
-
100
- const init = Bun.spawnSync([initdb, "-D", dataDir, "-U", "postgres", "-A", "trust"]);
101
- if (init.exitCode !== 0) {
102
- throw new Error(new TextDecoder().decode(init.stderr) || new TextDecoder().decode(init.stdout));
103
- }
104
-
105
- // Configure: local socket trust, TCP scram.
106
- const hbaPath = path.join(dataDir, "pg_hba.conf");
107
- fs.appendFileSync(
108
- hbaPath,
109
- "\n# Added by postgresai init integration tests\nlocal all all trust\nhost all all 127.0.0.1/32 scram-sha-256\nhost all all ::1/128 scram-sha-256\n",
110
- "utf8"
111
- );
112
-
113
- const port = await getFreePort();
114
-
115
- const postgresProc = Bun.spawn(
116
- [postgresBin, "-D", dataDir, "-k", socketDir, "-h", "127.0.0.1", "-p", String(port)],
117
- { stdio: ["ignore", "pipe", "pipe"] }
118
- );
119
-
120
- const cleanup = async () => {
121
- postgresProc.kill("SIGTERM");
122
- try {
123
- // 30s timeout to handle slower CI environments gracefully
124
- await waitFor(
125
- async () => {
126
- if (postgresProc.exitCode === null) throw new Error("still running");
127
- },
128
- { timeoutMs: 30000, intervalMs: 100 }
129
- );
130
- } catch {
131
- postgresProc.kill("SIGKILL");
132
- }
133
- fs.rmSync(tmpRoot, { recursive: true, force: true });
134
- };
135
-
136
- const connectLocal = async (database = "postgres"): Promise<Client> => {
137
- const c = new Client({ host: socketDir, port, user: "postgres", database });
138
- await c.connect();
139
- return c;
140
- };
141
-
142
- // Wait for Postgres to start (30s timeout for slower CI environments)
143
- await waitFor(async () => {
144
- const c = await connectLocal();
145
- await c.end();
146
- }, { timeoutMs: 30000, intervalMs: 100 });
147
-
148
- const postgresPassword = "postgrespw";
149
- {
150
- const c = await connectLocal();
151
- await c.query(`alter user postgres password ${sqlLiteral(postgresPassword)};`);
152
- await c.query("create database testdb");
153
- await c.end();
154
- }
155
-
156
- const adminUri = `postgresql://postgres:${postgresPassword}@127.0.0.1:${port}/testdb`;
157
- return { port, socketDir, adminUri, postgresPassword, cleanup };
158
- }
159
-
160
- function runCliInit(
161
- args: string[],
162
- env: Record<string, string> = {}
163
- ): { status: number | null; stdout: string; stderr: string } {
164
- const cliPath = path.resolve(import.meta.dir, "..", "bin", "postgres-ai.ts");
165
- const result = Bun.spawnSync(["bun", cliPath, "prepare-db", ...args], {
166
- env: { ...process.env, ...env },
167
- });
168
- return {
169
- status: result.exitCode,
170
- stdout: new TextDecoder().decode(result.stdout),
171
- stderr: new TextDecoder().decode(result.stderr),
172
- };
173
- }
174
-
175
- // Skip all tests if PostgreSQL binaries are not available or running as root
176
- // (initdb cannot be run as root)
177
- const skipTests = !havePostgresBinaries() || isRunningAsRoot();
178
-
179
- describe.skipIf(skipTests)("integration: prepare-db", () => {
180
- let pg: TempPostgres;
181
-
182
- // Use a shared postgres instance for all tests in this describe block
183
- // Each test will reset state as needed
184
-
185
- test("supports URI / conninfo / psql-like connection styles", async () => {
186
- pg = await createTempPostgres();
187
-
188
- try {
189
- // 1) positional URI
190
- {
191
- const r = runCliInit([pg.adminUri, "--password", "monpw", "--skip-optional-permissions"]);
192
- expect(r.status).toBe(0);
193
- }
194
-
195
- // 2) conninfo
196
- {
197
- const conninfo = `dbname=testdb host=127.0.0.1 port=${pg.port} user=postgres password=${pg.postgresPassword}`;
198
- const r = runCliInit([conninfo, "--password", "monpw2", "--skip-optional-permissions"]);
199
- expect(r.status).toBe(0);
200
- }
201
-
202
- // 3) psql-like options (+ PGPASSWORD)
203
- {
204
- const r = runCliInit(
205
- [
206
- "-h", "127.0.0.1",
207
- "-p", String(pg.port),
208
- "-U", "postgres",
209
- "-d", "testdb",
210
- "--password", "monpw3",
211
- "--skip-optional-permissions",
212
- ],
213
- { PGPASSWORD: pg.postgresPassword }
214
- );
215
- expect(r.status).toBe(0);
216
- }
217
- } finally {
218
- await pg.cleanup();
219
- }
220
- }, { timeout: TEST_TIMEOUT });
221
-
222
- test("requires explicit monitoring password in non-interactive mode", async () => {
223
- pg = await createTempPostgres();
224
-
225
- try {
226
- // Should fail without --print-password in non-interactive mode
227
- {
228
- const r = runCliInit([pg.adminUri, "--skip-optional-permissions"]);
229
- expect(r.status).not.toBe(0);
230
- expect(r.stderr).toMatch(/not printed in non-interactive mode/i);
231
- expect(r.stderr).toMatch(/--print-password/);
232
- }
233
-
234
- // With explicit opt-in, it should succeed
235
- {
236
- const r = runCliInit([pg.adminUri, "--print-password", "--skip-optional-permissions"]);
237
- expect(r.status).toBe(0);
238
- expect(r.stderr).toMatch(/Generated monitoring password for postgres_ai_mon/i);
239
- expect(r.stderr).toMatch(/PGAI_MON_PASSWORD=/);
240
- }
241
- } finally {
242
- await pg.cleanup();
243
- }
244
- }, { timeout: TEST_TIMEOUT });
245
-
246
- test(
247
- "fixes slightly-off permissions idempotently",
248
- async () => {
249
- pg = await createTempPostgres();
250
-
251
- try {
252
- // Create monitoring role with wrong password, no grants.
253
- {
254
- const c = new Client({ connectionString: pg.adminUri });
255
- await c.connect();
256
- await c.query(
257
- "do $$ begin if not exists (select 1 from pg_roles where rolname='postgres_ai_mon') then create role postgres_ai_mon login password 'wrong'; end if; end $$;"
258
- );
259
- await c.end();
260
- }
261
-
262
- // Run init (should grant everything).
263
- {
264
- const r = runCliInit([pg.adminUri, "--password", "correctpw", "--skip-optional-permissions"]);
265
- expect(r.status).toBe(0);
266
- }
267
-
268
- // Verify privileges.
269
- {
270
- const c = new Client({ connectionString: pg.adminUri });
271
- await c.connect();
272
- const dbOk = await c.query(
273
- "select has_database_privilege('postgres_ai_mon', current_database(), 'CONNECT') as ok"
274
- );
275
- expect(dbOk.rows[0].ok).toBe(true);
276
- const roleOk = await c.query("select pg_has_role('postgres_ai_mon', 'pg_monitor', 'member') as ok");
277
- expect(roleOk.rows[0].ok).toBe(true);
278
- const idxOk = await c.query(
279
- "select has_table_privilege('postgres_ai_mon', 'pg_catalog.pg_index', 'SELECT') as ok"
280
- );
281
- expect(idxOk.rows[0].ok).toBe(true);
282
- const viewOk = await c.query(
283
- "select has_table_privilege('postgres_ai_mon', 'postgres_ai.pg_statistic', 'SELECT') as ok"
284
- );
285
- expect(viewOk.rows[0].ok).toBe(true);
286
- const sp = await c.query("select rolconfig from pg_roles where rolname='postgres_ai_mon'");
287
- expect(Array.isArray(sp.rows[0].rolconfig)).toBe(true);
288
- expect(sp.rows[0].rolconfig.some((v: string) => String(v).includes("search_path="))).toBe(true);
289
- await c.end();
290
- }
291
-
292
- // Run init again (idempotent).
293
- {
294
- const r = runCliInit([pg.adminUri, "--password", "correctpw", "--skip-optional-permissions"]);
295
- expect(r.status).toBe(0);
296
- }
297
- } finally {
298
- await pg.cleanup();
299
- }
300
- },
301
- { timeout: TEST_TIMEOUT }
302
- );
303
-
304
- test("reports nicely when lacking permissions", async () => {
305
- pg = await createTempPostgres();
306
-
307
- try {
308
- // Create limited user that can connect but cannot create roles / grant.
309
- const limitedPw = "limitedpw";
310
- {
311
- const c = new Client({ connectionString: pg.adminUri });
312
- await c.connect();
313
- await c.query(`do $$ begin
314
- if not exists (select 1 from pg_roles where rolname='limited') then
315
- begin
316
- create role limited login password ${sqlLiteral(limitedPw)};
317
- exception when duplicate_object then
318
- null;
319
- end;
320
- end if;
321
- end $$;`);
322
- await c.query("grant connect on database testdb to limited");
323
- await c.end();
324
- }
325
-
326
- const limitedUri = `postgresql://limited:${limitedPw}@127.0.0.1:${pg.port}/testdb`;
327
- const r = runCliInit([limitedUri, "--password", "monpw", "--skip-optional-permissions"]);
328
- expect(r.status).not.toBe(0);
329
- expect(r.stderr).toMatch(/Error: prepare-db:/);
330
- expect(r.stderr).toMatch(/Failed at step "/);
331
- expect(r.stderr).toMatch(/Fix: connect as a superuser/i);
332
- } finally {
333
- await pg.cleanup();
334
- }
335
- }, { timeout: TEST_TIMEOUT });
336
-
337
- test(
338
- "--verify returns 0 when ok and non-zero when missing",
339
- async () => {
340
- pg = await createTempPostgres();
341
-
342
- try {
343
- // Prepare: run init
344
- {
345
- const r = runCliInit([pg.adminUri, "--password", "monpw", "--skip-optional-permissions"]);
346
- expect(r.status).toBe(0);
347
- }
348
-
349
- // Verify should pass
350
- {
351
- const r = runCliInit([pg.adminUri, "--verify", "--skip-optional-permissions"]);
352
- expect(r.status).toBe(0);
353
- expect(r.stdout).toMatch(/prepare-db verify: OK/i);
354
- }
355
-
356
- // Break a required privilege and ensure verify fails
357
- {
358
- const c = new Client({ connectionString: pg.adminUri });
359
- await c.connect();
360
- await c.query("revoke select on pg_catalog.pg_index from public");
361
- await c.query("revoke select on pg_catalog.pg_index from postgres_ai_mon");
362
- await c.end();
363
- }
364
- {
365
- const r = runCliInit([pg.adminUri, "--verify", "--skip-optional-permissions"]);
366
- expect(r.status).not.toBe(0);
367
- expect(r.stderr).toMatch(/prepare-db verify failed/i);
368
- expect(r.stderr).toMatch(/pg_catalog\.pg_index/i);
369
- }
370
- } finally {
371
- await pg.cleanup();
372
- }
373
- },
374
- { timeout: TEST_TIMEOUT }
375
- );
376
-
377
- // 15s timeout for PostgreSQL startup + two CLI init commands in slow CI
378
- test("--reset-password updates the monitoring role login password", async () => {
379
- pg = await createTempPostgres();
380
-
381
- try {
382
- // Initial setup with password pw1
383
- {
384
- const r = runCliInit([pg.adminUri, "--password", "pw1", "--skip-optional-permissions"]);
385
- expect(r.status).toBe(0);
386
- }
387
-
388
- // Reset to pw2
389
- {
390
- const r = runCliInit([pg.adminUri, "--reset-password", "--password", "pw2", "--skip-optional-permissions"]);
391
- expect(r.status).toBe(0);
392
- expect(r.stdout).toMatch(/password reset/i);
393
- }
394
-
395
- // Connect as monitoring user with new password should work
396
- {
397
- const c = new Client({
398
- connectionString: `postgresql://postgres_ai_mon:pw2@127.0.0.1:${pg.port}/testdb`,
399
- });
400
- await c.connect();
401
- const ok = await c.query("select 1 as ok");
402
- expect(ok.rows[0].ok).toBe(1);
403
- await c.end();
404
- }
405
- } finally {
406
- await pg.cleanup();
407
- }
408
- }, { timeout: TEST_TIMEOUT });
409
-
410
- // Security regression for #387: explain_generic was a SECURITY DEFINER helper that
411
- // ran caller-supplied SQL through EXPLAIN. The planner folds IMMUTABLE/STABLE functions
412
- // at plan time in the definer's (superuser) context, making it an RCE/data-exfil vector.
413
- // It has been removed; prepare-db must NOT create it.
414
- test("explain_generic is not created by prepare-db (RCE helper removed, #387)", async () => {
415
- pg = await createTempPostgres();
416
-
417
- try {
418
- {
419
- const r = runCliInit([pg.adminUri, "--password", "pw1", "--skip-optional-permissions"]);
420
- expect(r.status).toBe(0);
421
- }
422
-
423
- const c = new Client({ connectionString: pg.adminUri });
424
- await c.connect();
425
-
426
- try {
427
- const res = await c.query(
428
- "select count(*)::int as n from pg_proc where proname = 'explain_generic' and pronamespace = (select oid from pg_namespace where nspname = 'postgres_ai')"
429
- );
430
- expect(res.rows[0].n).toBe(0);
431
- } finally {
432
- await c.end();
433
- }
434
- } finally {
435
- await pg.cleanup();
436
- }
437
- }, { timeout: 60000 });
438
- });