postgresai 0.14.0-dev.43 → 0.14.0-dev.45
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/postgres-ai.ts +649 -310
- package/bun.lock +258 -0
- package/dist/bin/postgres-ai.js +29491 -1910
- package/dist/sql/01.role.sql +16 -0
- package/dist/sql/02.permissions.sql +37 -0
- package/dist/sql/03.optional_rds.sql +6 -0
- package/dist/sql/04.optional_self_managed.sql +8 -0
- package/dist/sql/05.helpers.sql +415 -0
- package/lib/auth-server.ts +58 -97
- package/lib/checkup-api.ts +175 -0
- package/lib/checkup.ts +837 -0
- package/lib/config.ts +3 -0
- package/lib/init.ts +106 -74
- package/lib/issues.ts +121 -194
- package/lib/mcp-server.ts +6 -17
- package/lib/metrics-loader.ts +156 -0
- package/package.json +13 -9
- package/sql/02.permissions.sql +9 -5
- package/sql/05.helpers.sql +415 -0
- package/test/checkup.test.ts +953 -0
- package/test/init.integration.test.ts +396 -0
- package/test/init.test.ts +345 -0
- package/test/schema-validation.test.ts +188 -0
- package/tsconfig.json +12 -20
- package/dist/bin/postgres-ai.d.ts +0 -3
- package/dist/bin/postgres-ai.d.ts.map +0 -1
- package/dist/bin/postgres-ai.js.map +0 -1
- package/dist/lib/auth-server.d.ts +0 -31
- package/dist/lib/auth-server.d.ts.map +0 -1
- package/dist/lib/auth-server.js +0 -263
- package/dist/lib/auth-server.js.map +0 -1
- package/dist/lib/config.d.ts +0 -45
- package/dist/lib/config.d.ts.map +0 -1
- package/dist/lib/config.js +0 -181
- package/dist/lib/config.js.map +0 -1
- package/dist/lib/init.d.ts +0 -85
- package/dist/lib/init.d.ts.map +0 -1
- package/dist/lib/init.js +0 -644
- package/dist/lib/init.js.map +0 -1
- package/dist/lib/issues.d.ts +0 -75
- package/dist/lib/issues.d.ts.map +0 -1
- package/dist/lib/issues.js +0 -336
- package/dist/lib/issues.js.map +0 -1
- package/dist/lib/mcp-server.d.ts +0 -9
- package/dist/lib/mcp-server.d.ts.map +0 -1
- package/dist/lib/mcp-server.js +0 -168
- package/dist/lib/mcp-server.js.map +0 -1
- package/dist/lib/pkce.d.ts +0 -32
- package/dist/lib/pkce.d.ts.map +0 -1
- package/dist/lib/pkce.js +0 -101
- package/dist/lib/pkce.js.map +0 -1
- package/dist/lib/util.d.ts +0 -27
- package/dist/lib/util.d.ts.map +0 -1
- package/dist/lib/util.js +0 -46
- package/dist/lib/util.js.map +0 -1
- package/dist/package.json +0 -46
- package/test/init.integration.test.cjs +0 -382
- package/test/init.test.cjs +0 -392
package/dist/lib/pkce.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"pkce.d.ts","sourceRoot":"","sources":["../../lib/pkce.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,KAAK,EAAE,MAAM,CAAC;CACf;AAyBD;;;GAGG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,CAE7C;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAG9D;AAED;;;GAGG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,IAAI,UAAU,CAW/C"}
|
package/dist/lib/pkce.js
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.generateCodeVerifier = generateCodeVerifier;
|
|
37
|
-
exports.generateCodeChallenge = generateCodeChallenge;
|
|
38
|
-
exports.generateState = generateState;
|
|
39
|
-
exports.generatePKCEParams = generatePKCEParams;
|
|
40
|
-
const crypto = __importStar(require("crypto"));
|
|
41
|
-
/**
|
|
42
|
-
* Generate a cryptographically random string for PKCE
|
|
43
|
-
* @param length - Length of the string (43-128 characters per RFC 7636)
|
|
44
|
-
* @returns Base64URL-encoded random string
|
|
45
|
-
*/
|
|
46
|
-
function generateRandomString(length = 64) {
|
|
47
|
-
const bytes = crypto.randomBytes(length);
|
|
48
|
-
return base64URLEncode(bytes);
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Base64URL encode (without padding)
|
|
52
|
-
* @param buffer - Buffer to encode
|
|
53
|
-
* @returns Base64URL-encoded string
|
|
54
|
-
*/
|
|
55
|
-
function base64URLEncode(buffer) {
|
|
56
|
-
return buffer
|
|
57
|
-
.toString("base64")
|
|
58
|
-
.replace(/\+/g, "-")
|
|
59
|
-
.replace(/\//g, "_")
|
|
60
|
-
.replace(/=/g, "");
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Generate PKCE code verifier
|
|
64
|
-
* @returns Random code verifier (43-128 characters)
|
|
65
|
-
*/
|
|
66
|
-
function generateCodeVerifier() {
|
|
67
|
-
return generateRandomString(32); // 32 bytes = 43 chars after base64url encoding
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* Generate PKCE code challenge from verifier
|
|
71
|
-
* Uses S256 method (SHA256)
|
|
72
|
-
* @param verifier - Code verifier string
|
|
73
|
-
* @returns Base64URL-encoded SHA256 hash of verifier
|
|
74
|
-
*/
|
|
75
|
-
function generateCodeChallenge(verifier) {
|
|
76
|
-
const hash = crypto.createHash("sha256").update(verifier).digest();
|
|
77
|
-
return base64URLEncode(hash);
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* Generate random state for CSRF protection
|
|
81
|
-
* @returns Random state string
|
|
82
|
-
*/
|
|
83
|
-
function generateState() {
|
|
84
|
-
return generateRandomString(16); // 16 bytes = 22 chars
|
|
85
|
-
}
|
|
86
|
-
/**
|
|
87
|
-
* Generate complete PKCE parameters
|
|
88
|
-
* @returns Object with verifier, challenge, challengeMethod, and state
|
|
89
|
-
*/
|
|
90
|
-
function generatePKCEParams() {
|
|
91
|
-
const verifier = generateCodeVerifier();
|
|
92
|
-
const challenge = generateCodeChallenge(verifier);
|
|
93
|
-
const state = generateState();
|
|
94
|
-
return {
|
|
95
|
-
codeVerifier: verifier,
|
|
96
|
-
codeChallenge: challenge,
|
|
97
|
-
codeChallengeMethod: "S256",
|
|
98
|
-
state: state,
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
//# sourceMappingURL=pkce.js.map
|
package/dist/lib/pkce.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"pkce.js","sourceRoot":"","sources":["../../lib/pkce.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuCA,oDAEC;AAQD,sDAGC;AAMD,sCAEC;AAMD,gDAWC;AA7ED,+CAAiC;AAYjC;;;;GAIG;AACH,SAAS,oBAAoB,CAAC,SAAiB,EAAE;IAC/C,MAAM,KAAK,GAAG,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;IACzC,OAAO,eAAe,CAAC,KAAK,CAAC,CAAC;AAChC,CAAC;AAED;;;;GAIG;AACH,SAAS,eAAe,CAAC,MAAc;IACrC,OAAO,MAAM;SACV,QAAQ,CAAC,QAAQ,CAAC;SAClB,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC;SACnB,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC;SACnB,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;AACvB,CAAC;AAED;;;GAGG;AACH,SAAgB,oBAAoB;IAClC,OAAO,oBAAoB,CAAC,EAAE,CAAC,CAAC,CAAC,+CAA+C;AAClF,CAAC;AAED;;;;;GAKG;AACH,SAAgB,qBAAqB,CAAC,QAAgB;IACpD,MAAM,IAAI,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,MAAM,EAAE,CAAC;IACnE,OAAO,eAAe,CAAC,IAAI,CAAC,CAAC;AAC/B,CAAC;AAED;;;GAGG;AACH,SAAgB,aAAa;IAC3B,OAAO,oBAAoB,CAAC,EAAE,CAAC,CAAC,CAAC,sBAAsB;AACzD,CAAC;AAED;;;GAGG;AACH,SAAgB,kBAAkB;IAChC,MAAM,QAAQ,GAAG,oBAAoB,EAAE,CAAC;IACxC,MAAM,SAAS,GAAG,qBAAqB,CAAC,QAAQ,CAAC,CAAC;IAClD,MAAM,KAAK,GAAG,aAAa,EAAE,CAAC;IAE9B,OAAO;QACL,YAAY,EAAE,QAAQ;QACtB,aAAa,EAAE,SAAS;QACxB,mBAAmB,EAAE,MAAM;QAC3B,KAAK,EAAE,KAAK;KACb,CAAC;AACJ,CAAC"}
|
package/dist/lib/util.d.ts
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
export declare function maskSecret(secret: string): string;
|
|
2
|
-
export interface RootOptsLike {
|
|
3
|
-
apiBaseUrl?: string;
|
|
4
|
-
uiBaseUrl?: string;
|
|
5
|
-
}
|
|
6
|
-
export interface ConfigLike {
|
|
7
|
-
baseUrl?: string | null;
|
|
8
|
-
}
|
|
9
|
-
export interface ResolvedBaseUrls {
|
|
10
|
-
apiBaseUrl: string;
|
|
11
|
-
uiBaseUrl: string;
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* Normalize a base URL by trimming a single trailing slash and validating.
|
|
15
|
-
* @throws Error if the URL is invalid
|
|
16
|
-
*/
|
|
17
|
-
export declare function normalizeBaseUrl(value: string): string;
|
|
18
|
-
/**
|
|
19
|
-
* Resolve API and UI base URLs using precedence and normalize them.
|
|
20
|
-
* Precedence (API): opts.apiBaseUrl → env.PGAI_API_BASE_URL → cfg.baseUrl → default
|
|
21
|
-
* Precedence (UI): opts.uiBaseUrl → env.PGAI_UI_BASE_URL → default
|
|
22
|
-
*/
|
|
23
|
-
export declare function resolveBaseUrls(opts?: RootOptsLike, cfg?: ConfigLike, defaults?: {
|
|
24
|
-
apiBaseUrl?: string;
|
|
25
|
-
uiBaseUrl?: string;
|
|
26
|
-
}): ResolvedBaseUrls;
|
|
27
|
-
//# sourceMappingURL=util.d.ts.map
|
package/dist/lib/util.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../../lib/util.ts"],"names":[],"mappings":"AAAA,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAKjD;AAGD,MAAM,WAAW,YAAY;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAUtD;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,IAAI,CAAC,EAAE,YAAY,EACnB,GAAG,CAAC,EAAE,UAAU,EAChB,QAAQ,GAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GACzD,gBAAgB,CAWlB"}
|
package/dist/lib/util.js
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.maskSecret = maskSecret;
|
|
4
|
-
exports.normalizeBaseUrl = normalizeBaseUrl;
|
|
5
|
-
exports.resolveBaseUrls = resolveBaseUrls;
|
|
6
|
-
function maskSecret(secret) {
|
|
7
|
-
if (!secret)
|
|
8
|
-
return "";
|
|
9
|
-
if (secret.length <= 8)
|
|
10
|
-
return "****";
|
|
11
|
-
if (secret.length <= 16)
|
|
12
|
-
return `${secret.slice(0, 4)}${"*".repeat(secret.length - 8)}${secret.slice(-4)}`;
|
|
13
|
-
return `${secret.slice(0, Math.min(12, secret.length - 8))}${"*".repeat(Math.max(4, secret.length - 16))}${secret.slice(-4)}`;
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* Normalize a base URL by trimming a single trailing slash and validating.
|
|
17
|
-
* @throws Error if the URL is invalid
|
|
18
|
-
*/
|
|
19
|
-
function normalizeBaseUrl(value) {
|
|
20
|
-
const trimmed = (value || "").replace(/\/$/, "");
|
|
21
|
-
try {
|
|
22
|
-
// Validate
|
|
23
|
-
// eslint-disable-next-line no-new
|
|
24
|
-
new URL(trimmed);
|
|
25
|
-
}
|
|
26
|
-
catch {
|
|
27
|
-
throw new Error(`Invalid base URL: ${value}`);
|
|
28
|
-
}
|
|
29
|
-
return trimmed;
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* Resolve API and UI base URLs using precedence and normalize them.
|
|
33
|
-
* Precedence (API): opts.apiBaseUrl → env.PGAI_API_BASE_URL → cfg.baseUrl → default
|
|
34
|
-
* Precedence (UI): opts.uiBaseUrl → env.PGAI_UI_BASE_URL → default
|
|
35
|
-
*/
|
|
36
|
-
function resolveBaseUrls(opts, cfg, defaults = {}) {
|
|
37
|
-
const defApi = defaults.apiBaseUrl || "https://postgres.ai/api/general/";
|
|
38
|
-
const defUi = defaults.uiBaseUrl || "https://console.postgres.ai";
|
|
39
|
-
const apiCandidate = (opts?.apiBaseUrl || process.env.PGAI_API_BASE_URL || cfg?.baseUrl || defApi);
|
|
40
|
-
const uiCandidate = (opts?.uiBaseUrl || process.env.PGAI_UI_BASE_URL || defUi);
|
|
41
|
-
return {
|
|
42
|
-
apiBaseUrl: normalizeBaseUrl(apiCandidate),
|
|
43
|
-
uiBaseUrl: normalizeBaseUrl(uiCandidate),
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
//# sourceMappingURL=util.js.map
|
package/dist/lib/util.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"util.js","sourceRoot":"","sources":["../../lib/util.ts"],"names":[],"mappings":";;AAAA,gCAKC;AAqBD,4CAUC;AAOD,0CAeC;AA1DD,SAAgB,UAAU,CAAC,MAAc;IACvC,IAAI,CAAC,MAAM;QAAE,OAAO,EAAE,CAAC;IACvB,IAAI,MAAM,CAAC,MAAM,IAAI,CAAC;QAAE,OAAO,MAAM,CAAC;IACtC,IAAI,MAAM,CAAC,MAAM,IAAI,EAAE;QAAE,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAC3G,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AAChI,CAAC;AAiBD;;;GAGG;AACH,SAAgB,gBAAgB,CAAC,KAAa;IAC5C,MAAM,OAAO,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACjD,IAAI,CAAC;QACH,WAAW;QACX,kCAAkC;QAClC,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC;IACnB,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CAAC,qBAAqB,KAAK,EAAE,CAAC,CAAC;IAChD,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;GAIG;AACH,SAAgB,eAAe,CAC7B,IAAmB,EACnB,GAAgB,EAChB,WAAwD,EAAE;IAE1D,MAAM,MAAM,GAAG,QAAQ,CAAC,UAAU,IAAI,kCAAkC,CAAC;IACzE,MAAM,KAAK,GAAG,QAAQ,CAAC,SAAS,IAAI,6BAA6B,CAAC;IAElE,MAAM,YAAY,GAAG,CAAC,IAAI,EAAE,UAAU,IAAI,OAAO,CAAC,GAAG,CAAC,iBAAiB,IAAI,GAAG,EAAE,OAAO,IAAI,MAAM,CAAW,CAAC;IAC7G,MAAM,WAAW,GAAG,CAAC,IAAI,EAAE,SAAS,IAAI,OAAO,CAAC,GAAG,CAAC,gBAAgB,IAAI,KAAK,CAAW,CAAC;IAEzF,OAAO;QACL,UAAU,EAAE,gBAAgB,CAAC,YAAY,CAAC;QAC1C,SAAS,EAAE,gBAAgB,CAAC,WAAW,CAAC;KACzC,CAAC;AACJ,CAAC"}
|
package/dist/package.json
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "postgresai",
|
|
3
|
-
"version": "0.14.0-dev.43",
|
|
4
|
-
"description": "postgres_ai CLI (Node.js)",
|
|
5
|
-
"license": "Apache-2.0",
|
|
6
|
-
"private": false,
|
|
7
|
-
"repository": {
|
|
8
|
-
"type": "git",
|
|
9
|
-
"url": "git+https://gitlab.com/postgres-ai/postgres_ai.git"
|
|
10
|
-
},
|
|
11
|
-
"homepage": "https://gitlab.com/postgres-ai/postgres_ai",
|
|
12
|
-
"bugs": {
|
|
13
|
-
"url": "https://gitlab.com/postgres-ai/postgres_ai/-/issues"
|
|
14
|
-
},
|
|
15
|
-
"bin": {
|
|
16
|
-
"postgres-ai": "./dist/bin/postgres-ai.js",
|
|
17
|
-
"postgresai": "./dist/bin/postgres-ai.js",
|
|
18
|
-
"pgai": "./dist/bin/postgres-ai.js"
|
|
19
|
-
},
|
|
20
|
-
"type": "commonjs",
|
|
21
|
-
"engines": {
|
|
22
|
-
"node": ">=18"
|
|
23
|
-
},
|
|
24
|
-
"scripts": {
|
|
25
|
-
"build": "tsc",
|
|
26
|
-
"prepare": "npm run build",
|
|
27
|
-
"start": "node ./dist/bin/postgres-ai.js --help",
|
|
28
|
-
"dev": "tsc --watch",
|
|
29
|
-
"test": "npm run build && node --test test/*.test.cjs"
|
|
30
|
-
},
|
|
31
|
-
"dependencies": {
|
|
32
|
-
"@modelcontextprotocol/sdk": "^1.20.2",
|
|
33
|
-
"commander": "^12.1.0",
|
|
34
|
-
"js-yaml": "^4.1.0",
|
|
35
|
-
"pg": "^8.16.3"
|
|
36
|
-
},
|
|
37
|
-
"devDependencies": {
|
|
38
|
-
"@types/js-yaml": "^4.0.9",
|
|
39
|
-
"@types/node": "^18.19.0",
|
|
40
|
-
"@types/pg": "^8.15.6",
|
|
41
|
-
"typescript": "^5.3.3"
|
|
42
|
-
},
|
|
43
|
-
"publishConfig": {
|
|
44
|
-
"access": "public"
|
|
45
|
-
}
|
|
46
|
-
}
|
|
@@ -1,382 +0,0 @@
|
|
|
1
|
-
const test = require("node:test");
|
|
2
|
-
const assert = require("node:assert/strict");
|
|
3
|
-
const fs = require("node:fs");
|
|
4
|
-
const os = require("node:os");
|
|
5
|
-
const path = require("node:path");
|
|
6
|
-
const net = require("node:net");
|
|
7
|
-
const { spawn, spawnSync } = require("node:child_process");
|
|
8
|
-
|
|
9
|
-
function sqlLiteral(value) {
|
|
10
|
-
return `'${String(value).replace(/'/g, "''")}'`;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function findOnPath(cmd) {
|
|
14
|
-
const which = spawnSync("sh", ["-lc", `command -v ${cmd}`], { encoding: "utf8" });
|
|
15
|
-
if (which.status === 0) return String(which.stdout || "").trim();
|
|
16
|
-
return null;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function findPgBin(cmd) {
|
|
20
|
-
const p = findOnPath(cmd);
|
|
21
|
-
if (p) return p;
|
|
22
|
-
|
|
23
|
-
// Debian/Ubuntu (GitLab CI node:*-bullseye images): binaries usually live here.
|
|
24
|
-
// We avoid filesystem globbing in JS and just ask the shell.
|
|
25
|
-
const probe = spawnSync(
|
|
26
|
-
"sh",
|
|
27
|
-
[
|
|
28
|
-
"-lc",
|
|
29
|
-
`ls -1 /usr/lib/postgresql/*/bin/${cmd} 2>/dev/null | head -n 1 || true`,
|
|
30
|
-
],
|
|
31
|
-
{ encoding: "utf8" }
|
|
32
|
-
);
|
|
33
|
-
const out = String(probe.stdout || "").trim();
|
|
34
|
-
if (out) return out;
|
|
35
|
-
|
|
36
|
-
return null;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function havePostgresBinaries() {
|
|
40
|
-
return !!(findPgBin("initdb") && findPgBin("postgres"));
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async function getFreePort() {
|
|
44
|
-
return await new Promise((resolve, reject) => {
|
|
45
|
-
const srv = net.createServer();
|
|
46
|
-
srv.listen(0, "127.0.0.1", () => {
|
|
47
|
-
const addr = srv.address();
|
|
48
|
-
srv.close((err) => {
|
|
49
|
-
if (err) return reject(err);
|
|
50
|
-
resolve(addr.port);
|
|
51
|
-
});
|
|
52
|
-
});
|
|
53
|
-
srv.on("error", reject);
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
async function waitFor(fn, { timeoutMs = 10000, intervalMs = 100 } = {}) {
|
|
58
|
-
const start = Date.now();
|
|
59
|
-
// eslint-disable-next-line no-constant-condition
|
|
60
|
-
while (true) {
|
|
61
|
-
try {
|
|
62
|
-
return await fn();
|
|
63
|
-
} catch (e) {
|
|
64
|
-
if (Date.now() - start > timeoutMs) throw e;
|
|
65
|
-
await new Promise((r) => setTimeout(r, intervalMs));
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
async function withTempPostgres(t) {
|
|
71
|
-
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "postgresai-init-"));
|
|
72
|
-
const dataDir = path.join(tmpRoot, "data");
|
|
73
|
-
const socketDir = path.join(tmpRoot, "sock");
|
|
74
|
-
fs.mkdirSync(socketDir, { recursive: true });
|
|
75
|
-
|
|
76
|
-
const initdb = findPgBin("initdb");
|
|
77
|
-
const postgresBin = findPgBin("postgres");
|
|
78
|
-
assert.ok(initdb && postgresBin, "PostgreSQL binaries not found (need initdb and postgres)");
|
|
79
|
-
|
|
80
|
-
const init = spawnSync(initdb, ["-D", dataDir, "-U", "postgres", "-A", "trust"], {
|
|
81
|
-
encoding: "utf8",
|
|
82
|
-
});
|
|
83
|
-
assert.equal(init.status, 0, init.stderr || init.stdout);
|
|
84
|
-
|
|
85
|
-
// Configure: local socket trust, TCP scram.
|
|
86
|
-
const hbaPath = path.join(dataDir, "pg_hba.conf");
|
|
87
|
-
fs.appendFileSync(
|
|
88
|
-
hbaPath,
|
|
89
|
-
"\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",
|
|
90
|
-
"utf8"
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
const port = await getFreePort();
|
|
94
|
-
|
|
95
|
-
let postgresProc;
|
|
96
|
-
try {
|
|
97
|
-
postgresProc = spawn(
|
|
98
|
-
postgresBin,
|
|
99
|
-
["-D", dataDir, "-k", socketDir, "-h", "127.0.0.1", "-p", String(port)],
|
|
100
|
-
{
|
|
101
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
102
|
-
}
|
|
103
|
-
);
|
|
104
|
-
|
|
105
|
-
// Register cleanup immediately so failures below don't leave a running postgres and hang CI.
|
|
106
|
-
t.after(async () => {
|
|
107
|
-
postgresProc.kill("SIGTERM");
|
|
108
|
-
try {
|
|
109
|
-
await waitFor(
|
|
110
|
-
async () => {
|
|
111
|
-
if (postgresProc.exitCode === null) throw new Error("still running");
|
|
112
|
-
},
|
|
113
|
-
{ timeoutMs: 5000, intervalMs: 100 }
|
|
114
|
-
);
|
|
115
|
-
} catch {
|
|
116
|
-
postgresProc.kill("SIGKILL");
|
|
117
|
-
}
|
|
118
|
-
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
119
|
-
});
|
|
120
|
-
} catch (e) {
|
|
121
|
-
// If anything goes wrong before cleanup is registered, ensure we don't leak a running postgres.
|
|
122
|
-
try {
|
|
123
|
-
if (postgresProc) postgresProc.kill("SIGKILL");
|
|
124
|
-
} catch {
|
|
125
|
-
// ignore
|
|
126
|
-
}
|
|
127
|
-
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
128
|
-
throw e;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const { Client } = require("pg");
|
|
132
|
-
|
|
133
|
-
const connectLocal = async (database = "postgres") => {
|
|
134
|
-
// IMPORTANT: must match the port Postgres is started with; otherwise pg defaults to 5432 and the socket path won't exist.
|
|
135
|
-
const c = new Client({ host: socketDir, port, user: "postgres", database });
|
|
136
|
-
await c.connect();
|
|
137
|
-
return c;
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
await waitFor(async () => {
|
|
141
|
-
const c = await connectLocal();
|
|
142
|
-
await c.end();
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
const postgresPassword = "postgrespw";
|
|
146
|
-
{
|
|
147
|
-
const c = await connectLocal();
|
|
148
|
-
await c.query(`alter user postgres password ${sqlLiteral(postgresPassword)};`);
|
|
149
|
-
await c.query("create database testdb");
|
|
150
|
-
await c.end();
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const adminUri = `postgresql://postgres:${postgresPassword}@127.0.0.1:${port}/testdb`;
|
|
154
|
-
return { port, socketDir, adminUri, postgresPassword };
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
async function runCliInit(args, env = {}) {
|
|
158
|
-
const node = process.execPath;
|
|
159
|
-
const cliPath = path.resolve(__dirname, "..", "dist", "bin", "postgres-ai.js");
|
|
160
|
-
const res = spawnSync(node, [cliPath, "prepare-db", ...args], {
|
|
161
|
-
encoding: "utf8",
|
|
162
|
-
env: { ...process.env, ...env },
|
|
163
|
-
});
|
|
164
|
-
return res;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
test(
|
|
168
|
-
"integration: prepare-db supports URI / conninfo / psql-like connection styles",
|
|
169
|
-
{ skip: !havePostgresBinaries() },
|
|
170
|
-
async (t) => {
|
|
171
|
-
const pg = await withTempPostgres(t);
|
|
172
|
-
|
|
173
|
-
// 1) positional URI
|
|
174
|
-
{
|
|
175
|
-
const r = await runCliInit([pg.adminUri, "--password", "monpw", "--skip-optional-permissions"]);
|
|
176
|
-
assert.equal(r.status, 0, r.stderr || r.stdout);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// 2) conninfo
|
|
180
|
-
{
|
|
181
|
-
const conninfo = `dbname=testdb host=127.0.0.1 port=${pg.port} user=postgres password=${pg.postgresPassword}`;
|
|
182
|
-
const r = await runCliInit([conninfo, "--password", "monpw2", "--skip-optional-permissions"]);
|
|
183
|
-
assert.equal(r.status, 0, r.stderr || r.stdout);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// 3) psql-like options (+ PGPASSWORD)
|
|
187
|
-
{
|
|
188
|
-
const r = await runCliInit(
|
|
189
|
-
[
|
|
190
|
-
"-h",
|
|
191
|
-
"127.0.0.1",
|
|
192
|
-
"-p",
|
|
193
|
-
String(pg.port),
|
|
194
|
-
"-U",
|
|
195
|
-
"postgres",
|
|
196
|
-
"-d",
|
|
197
|
-
"testdb",
|
|
198
|
-
"--password",
|
|
199
|
-
"monpw3",
|
|
200
|
-
"--skip-optional-permissions",
|
|
201
|
-
],
|
|
202
|
-
{ PGPASSWORD: pg.postgresPassword }
|
|
203
|
-
);
|
|
204
|
-
assert.equal(r.status, 0, r.stderr || r.stdout);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
);
|
|
208
|
-
|
|
209
|
-
test(
|
|
210
|
-
"integration: prepare-db requires explicit monitoring password in non-interactive mode (unless --print-password)",
|
|
211
|
-
{ skip: !havePostgresBinaries() },
|
|
212
|
-
async (t) => {
|
|
213
|
-
const pg = await withTempPostgres(t);
|
|
214
|
-
|
|
215
|
-
// spawnSync captures stdout/stderr (non-TTY). We should not print a generated password unless explicitly requested.
|
|
216
|
-
{
|
|
217
|
-
const r = await runCliInit([pg.adminUri, "--skip-optional-permissions"]);
|
|
218
|
-
assert.notEqual(r.status, 0);
|
|
219
|
-
assert.match(r.stderr, /not printed in non-interactive mode/i);
|
|
220
|
-
assert.match(r.stderr, /--print-password/);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// With explicit opt-in, it should succeed (and will print the generated password).
|
|
224
|
-
{
|
|
225
|
-
const r = await runCliInit([pg.adminUri, "--print-password", "--skip-optional-permissions"]);
|
|
226
|
-
assert.equal(r.status, 0, r.stderr || r.stdout);
|
|
227
|
-
assert.match(r.stderr, /Generated monitoring password for postgres_ai_mon/i);
|
|
228
|
-
assert.match(r.stderr, /PGAI_MON_PASSWORD=/);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
);
|
|
232
|
-
|
|
233
|
-
test(
|
|
234
|
-
"integration: prepare-db fixes slightly-off permissions idempotently",
|
|
235
|
-
{ skip: !havePostgresBinaries() },
|
|
236
|
-
async (t) => {
|
|
237
|
-
const pg = await withTempPostgres(t);
|
|
238
|
-
const { Client } = require("pg");
|
|
239
|
-
|
|
240
|
-
// Create monitoring role with wrong password, no grants.
|
|
241
|
-
{
|
|
242
|
-
const c = new Client({ connectionString: pg.adminUri });
|
|
243
|
-
await c.connect();
|
|
244
|
-
await c.query(
|
|
245
|
-
"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 $$;"
|
|
246
|
-
);
|
|
247
|
-
await c.end();
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// Run init (should grant everything).
|
|
251
|
-
{
|
|
252
|
-
const r = await runCliInit([pg.adminUri, "--password", "correctpw", "--skip-optional-permissions"]);
|
|
253
|
-
assert.equal(r.status, 0, r.stderr || r.stdout);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// Verify privileges.
|
|
257
|
-
{
|
|
258
|
-
const c = new Client({ connectionString: pg.adminUri });
|
|
259
|
-
await c.connect();
|
|
260
|
-
const dbOk = await c.query(
|
|
261
|
-
"select has_database_privilege('postgres_ai_mon', current_database(), 'CONNECT') as ok"
|
|
262
|
-
);
|
|
263
|
-
assert.equal(dbOk.rows[0].ok, true);
|
|
264
|
-
const roleOk = await c.query("select pg_has_role('postgres_ai_mon', 'pg_monitor', 'member') as ok");
|
|
265
|
-
assert.equal(roleOk.rows[0].ok, true);
|
|
266
|
-
const idxOk = await c.query(
|
|
267
|
-
"select has_table_privilege('postgres_ai_mon', 'pg_catalog.pg_index', 'SELECT') as ok"
|
|
268
|
-
);
|
|
269
|
-
assert.equal(idxOk.rows[0].ok, true);
|
|
270
|
-
const viewOk = await c.query(
|
|
271
|
-
"select has_table_privilege('postgres_ai_mon', 'public.pg_statistic', 'SELECT') as ok"
|
|
272
|
-
);
|
|
273
|
-
assert.equal(viewOk.rows[0].ok, true);
|
|
274
|
-
const sp = await c.query("select rolconfig from pg_roles where rolname='postgres_ai_mon'");
|
|
275
|
-
assert.ok(Array.isArray(sp.rows[0].rolconfig));
|
|
276
|
-
assert.ok(sp.rows[0].rolconfig.some((v) => String(v).includes("search_path=")));
|
|
277
|
-
await c.end();
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// Run init again (idempotent).
|
|
281
|
-
{
|
|
282
|
-
const r = await runCliInit([pg.adminUri, "--password", "correctpw", "--skip-optional-permissions"]);
|
|
283
|
-
assert.equal(r.status, 0, r.stderr || r.stdout);
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
);
|
|
287
|
-
|
|
288
|
-
test("integration: prepare-db reports nicely when lacking permissions", { skip: !havePostgresBinaries() }, async (t) => {
|
|
289
|
-
const pg = await withTempPostgres(t);
|
|
290
|
-
const { Client } = require("pg");
|
|
291
|
-
|
|
292
|
-
// Create limited user that can connect but cannot create roles / grant.
|
|
293
|
-
const limitedPw = "limitedpw";
|
|
294
|
-
{
|
|
295
|
-
const c = new Client({ connectionString: pg.adminUri });
|
|
296
|
-
await c.connect();
|
|
297
|
-
await c.query(`do $$ begin
|
|
298
|
-
if not exists (select 1 from pg_roles where rolname='limited') then
|
|
299
|
-
begin
|
|
300
|
-
create role limited login password ${sqlLiteral(limitedPw)};
|
|
301
|
-
exception when duplicate_object then
|
|
302
|
-
null;
|
|
303
|
-
end;
|
|
304
|
-
end if;
|
|
305
|
-
end $$;`);
|
|
306
|
-
await c.query("grant connect on database testdb to limited");
|
|
307
|
-
await c.end();
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
const limitedUri = `postgresql://limited:${limitedPw}@127.0.0.1:${pg.port}/testdb`;
|
|
311
|
-
const r = await runCliInit([limitedUri, "--password", "monpw", "--skip-optional-permissions"]);
|
|
312
|
-
assert.notEqual(r.status, 0);
|
|
313
|
-
assert.match(r.stderr, /Error: prepare-db:/);
|
|
314
|
-
// Should include step context and hint.
|
|
315
|
-
assert.match(r.stderr, /Failed at step "/);
|
|
316
|
-
assert.match(r.stderr, /Fix: connect as a superuser/i);
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
test("integration: prepare-db --verify returns 0 when ok and non-zero when missing", { skip: !havePostgresBinaries() }, async (t) => {
|
|
320
|
-
const pg = await withTempPostgres(t);
|
|
321
|
-
const { Client } = require("pg");
|
|
322
|
-
|
|
323
|
-
// Prepare: run init
|
|
324
|
-
{
|
|
325
|
-
const r = await runCliInit([pg.adminUri, "--password", "monpw", "--skip-optional-permissions"]);
|
|
326
|
-
assert.equal(r.status, 0, r.stderr || r.stdout);
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// Verify should pass
|
|
330
|
-
{
|
|
331
|
-
const r = await runCliInit([pg.adminUri, "--verify", "--skip-optional-permissions"]);
|
|
332
|
-
assert.equal(r.status, 0, r.stderr || r.stdout);
|
|
333
|
-
assert.match(r.stdout, /prepare-db verify: OK/i);
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
// Break a required privilege and ensure verify fails
|
|
337
|
-
{
|
|
338
|
-
const c = new Client({ connectionString: pg.adminUri });
|
|
339
|
-
await c.connect();
|
|
340
|
-
// pg_catalog tables are often readable via PUBLIC by default; revoke from PUBLIC too so the failure is deterministic.
|
|
341
|
-
await c.query("revoke select on pg_catalog.pg_index from public");
|
|
342
|
-
await c.query("revoke select on pg_catalog.pg_index from postgres_ai_mon");
|
|
343
|
-
await c.end();
|
|
344
|
-
}
|
|
345
|
-
{
|
|
346
|
-
const r = await runCliInit([pg.adminUri, "--verify", "--skip-optional-permissions"]);
|
|
347
|
-
assert.notEqual(r.status, 0);
|
|
348
|
-
assert.match(r.stderr, /prepare-db verify failed/i);
|
|
349
|
-
assert.match(r.stderr, /pg_catalog\.pg_index/i);
|
|
350
|
-
}
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
test("integration: prepare-db --reset-password updates the monitoring role login password", { skip: !havePostgresBinaries() }, async (t) => {
|
|
354
|
-
const pg = await withTempPostgres(t);
|
|
355
|
-
const { Client } = require("pg");
|
|
356
|
-
|
|
357
|
-
// Initial setup with password pw1
|
|
358
|
-
{
|
|
359
|
-
const r = await runCliInit([pg.adminUri, "--password", "pw1", "--skip-optional-permissions"]);
|
|
360
|
-
assert.equal(r.status, 0, r.stderr || r.stdout);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
// Reset to pw2
|
|
364
|
-
{
|
|
365
|
-
const r = await runCliInit([pg.adminUri, "--reset-password", "--password", "pw2", "--skip-optional-permissions"]);
|
|
366
|
-
assert.equal(r.status, 0, r.stderr || r.stdout);
|
|
367
|
-
assert.match(r.stdout, /password reset/i);
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
// Connect as monitoring user with new password should work
|
|
371
|
-
{
|
|
372
|
-
const c = new Client({
|
|
373
|
-
connectionString: `postgresql://postgres_ai_mon:pw2@127.0.0.1:${pg.port}/testdb`,
|
|
374
|
-
});
|
|
375
|
-
await c.connect();
|
|
376
|
-
const ok = await c.query("select 1 as ok");
|
|
377
|
-
assert.equal(ok.rows[0].ok, 1);
|
|
378
|
-
await c.end();
|
|
379
|
-
}
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
|