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/lib/auth-server.ts
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import * as http from "http";
|
|
2
|
-
import { URL } from "url";
|
|
3
|
-
|
|
4
1
|
/**
|
|
5
2
|
* OAuth callback result
|
|
6
3
|
*/
|
|
@@ -13,7 +10,7 @@ export interface CallbackResult {
|
|
|
13
10
|
* Callback server structure
|
|
14
11
|
*/
|
|
15
12
|
export interface CallbackServer {
|
|
16
|
-
server:
|
|
13
|
+
server: { stop: () => void };
|
|
17
14
|
promise: Promise<CallbackResult>;
|
|
18
15
|
getPort: () => number;
|
|
19
16
|
}
|
|
@@ -34,11 +31,18 @@ function escapeHtml(str: string | null): string {
|
|
|
34
31
|
}
|
|
35
32
|
|
|
36
33
|
/**
|
|
37
|
-
* Create and start callback server
|
|
34
|
+
* Create and start callback server using Bun.serve
|
|
35
|
+
*
|
|
38
36
|
* @param port - Port to listen on (0 for random available port)
|
|
39
37
|
* @param expectedState - Expected state parameter for CSRF protection
|
|
40
38
|
* @param timeoutMs - Timeout in milliseconds
|
|
41
39
|
* @returns Server object with promise and getPort function
|
|
40
|
+
*
|
|
41
|
+
* @remarks
|
|
42
|
+
* The server stops asynchronously ~100ms after the callback resolves/rejects.
|
|
43
|
+
* This delay ensures the HTTP response is fully sent before closing the connection.
|
|
44
|
+
* Callers should not attempt to reuse the same port immediately after the promise
|
|
45
|
+
* resolves - wait at least 200ms or use a different port.
|
|
42
46
|
*/
|
|
43
47
|
export function createCallbackServer(
|
|
44
48
|
port: number = 0,
|
|
@@ -46,42 +50,42 @@ export function createCallbackServer(
|
|
|
46
50
|
timeoutMs: number = 300000
|
|
47
51
|
): CallbackServer {
|
|
48
52
|
let resolved = false;
|
|
49
|
-
let server: http.Server | null = null;
|
|
50
53
|
let actualPort = port;
|
|
51
54
|
let resolveCallback: (value: CallbackResult) => void;
|
|
52
55
|
let rejectCallback: (reason: Error) => void;
|
|
53
|
-
|
|
56
|
+
let serverInstance: ReturnType<typeof Bun.serve> | null = null;
|
|
57
|
+
|
|
54
58
|
const promise = new Promise<CallbackResult>((resolve, reject) => {
|
|
55
59
|
resolveCallback = resolve;
|
|
56
60
|
rejectCallback = reject;
|
|
57
61
|
});
|
|
58
|
-
|
|
62
|
+
|
|
59
63
|
// Timeout handler
|
|
60
64
|
const timeout = setTimeout(() => {
|
|
61
65
|
if (!resolved) {
|
|
62
66
|
resolved = true;
|
|
63
|
-
if (
|
|
64
|
-
|
|
67
|
+
if (serverInstance) {
|
|
68
|
+
serverInstance.stop();
|
|
65
69
|
}
|
|
66
70
|
rejectCallback(new Error("Authentication timeout. Please try again."));
|
|
67
71
|
}
|
|
68
72
|
}, timeoutMs);
|
|
69
|
-
|
|
70
|
-
// Request handler
|
|
71
|
-
const requestHandler = (req: http.IncomingMessage, res: http.ServerResponse): void => {
|
|
72
|
-
if (resolved) {
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
73
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
74
|
+
serverInstance = Bun.serve({
|
|
75
|
+
port: port,
|
|
76
|
+
hostname: "127.0.0.1",
|
|
77
|
+
fetch(req) {
|
|
78
|
+
if (resolved) {
|
|
79
|
+
return new Response("Already handled", { status: 200 });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const url = new URL(req.url);
|
|
83
|
+
|
|
84
|
+
// Only handle /callback path
|
|
85
|
+
if (!url.pathname.startsWith("/callback")) {
|
|
86
|
+
return new Response("Not Found", { status: 404 });
|
|
87
|
+
}
|
|
82
88
|
|
|
83
|
-
try {
|
|
84
|
-
const url = new URL(req.url, `http://localhost:${actualPort}`);
|
|
85
89
|
const code = url.searchParams.get("code");
|
|
86
90
|
const state = url.searchParams.get("state");
|
|
87
91
|
const error = url.searchParams.get("error");
|
|
@@ -91,9 +95,11 @@ export function createCallbackServer(
|
|
|
91
95
|
if (error) {
|
|
92
96
|
resolved = true;
|
|
93
97
|
clearTimeout(timeout);
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
98
|
+
|
|
99
|
+
setTimeout(() => serverInstance?.stop(), 100);
|
|
100
|
+
rejectCallback(new Error(`OAuth error: ${error}${errorDescription ? ` - ${errorDescription}` : ""}`));
|
|
101
|
+
|
|
102
|
+
return new Response(`
|
|
97
103
|
<!DOCTYPE html>
|
|
98
104
|
<html>
|
|
99
105
|
<head>
|
|
@@ -114,19 +120,12 @@ export function createCallbackServer(
|
|
|
114
120
|
</div>
|
|
115
121
|
</body>
|
|
116
122
|
</html>
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
if (server) {
|
|
120
|
-
server.close();
|
|
121
|
-
}
|
|
122
|
-
rejectCallback(new Error(`OAuth error: ${error}${errorDescription ? ` - ${errorDescription}` : ""}`));
|
|
123
|
-
return;
|
|
123
|
+
`, { status: 400, headers: { "Content-Type": "text/html" } });
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
// Validate required parameters
|
|
127
127
|
if (!code || !state) {
|
|
128
|
-
|
|
129
|
-
res.end(`
|
|
128
|
+
return new Response(`
|
|
130
129
|
<!DOCTYPE html>
|
|
131
130
|
<html>
|
|
132
131
|
<head>
|
|
@@ -145,17 +144,18 @@ export function createCallbackServer(
|
|
|
145
144
|
</div>
|
|
146
145
|
</body>
|
|
147
146
|
</html>
|
|
148
|
-
|
|
149
|
-
return;
|
|
147
|
+
`, { status: 400, headers: { "Content-Type": "text/html" } });
|
|
150
148
|
}
|
|
151
149
|
|
|
152
150
|
// Validate state (CSRF protection)
|
|
153
151
|
if (expectedState && state !== expectedState) {
|
|
154
152
|
resolved = true;
|
|
155
153
|
clearTimeout(timeout);
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
154
|
+
|
|
155
|
+
setTimeout(() => serverInstance?.stop(), 100);
|
|
156
|
+
rejectCallback(new Error("State mismatch (possible CSRF attack)"));
|
|
157
|
+
|
|
158
|
+
return new Response(`
|
|
159
159
|
<!DOCTYPE html>
|
|
160
160
|
<html>
|
|
161
161
|
<head>
|
|
@@ -174,21 +174,19 @@ export function createCallbackServer(
|
|
|
174
174
|
</div>
|
|
175
175
|
</body>
|
|
176
176
|
</html>
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
if (server) {
|
|
180
|
-
server.close();
|
|
181
|
-
}
|
|
182
|
-
rejectCallback(new Error("State mismatch (possible CSRF attack)"));
|
|
183
|
-
return;
|
|
177
|
+
`, { status: 400, headers: { "Content-Type": "text/html" } });
|
|
184
178
|
}
|
|
185
179
|
|
|
186
180
|
// Success!
|
|
187
181
|
resolved = true;
|
|
188
182
|
clearTimeout(timeout);
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
183
|
+
|
|
184
|
+
// Resolve first, then stop server asynchronously after response is sent.
|
|
185
|
+
// The 100ms delay ensures the HTTP response is fully written before closing.
|
|
186
|
+
resolveCallback({ code, state });
|
|
187
|
+
setTimeout(() => serverInstance?.stop(), 100);
|
|
188
|
+
|
|
189
|
+
return new Response(`
|
|
192
190
|
<!DOCTYPE html>
|
|
193
191
|
<html>
|
|
194
192
|
<head>
|
|
@@ -207,61 +205,24 @@ export function createCallbackServer(
|
|
|
207
205
|
</div>
|
|
208
206
|
</body>
|
|
209
207
|
</html>
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
if (server) {
|
|
213
|
-
server.close();
|
|
214
|
-
}
|
|
215
|
-
resolveCallback({ code, state });
|
|
216
|
-
} catch (err) {
|
|
217
|
-
if (!resolved) {
|
|
218
|
-
resolved = true;
|
|
219
|
-
clearTimeout(timeout);
|
|
220
|
-
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
221
|
-
res.end("Internal Server Error");
|
|
222
|
-
if (server) {
|
|
223
|
-
server.close();
|
|
224
|
-
}
|
|
225
|
-
rejectCallback(err instanceof Error ? err : new Error(String(err)));
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
};
|
|
229
|
-
|
|
230
|
-
// Create server
|
|
231
|
-
server = http.createServer(requestHandler);
|
|
232
|
-
|
|
233
|
-
server.on("error", (err: Error) => {
|
|
234
|
-
if (!resolved) {
|
|
235
|
-
resolved = true;
|
|
236
|
-
clearTimeout(timeout);
|
|
237
|
-
rejectCallback(err);
|
|
238
|
-
}
|
|
208
|
+
`, { status: 200, headers: { "Content-Type": "text/html" } });
|
|
209
|
+
},
|
|
239
210
|
});
|
|
240
211
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
if (address && typeof address === "object") {
|
|
244
|
-
actualPort = address.port;
|
|
245
|
-
}
|
|
246
|
-
});
|
|
247
|
-
|
|
212
|
+
actualPort = serverInstance.port;
|
|
213
|
+
|
|
248
214
|
return {
|
|
249
|
-
server,
|
|
215
|
+
server: { stop: () => serverInstance?.stop() },
|
|
250
216
|
promise,
|
|
251
|
-
getPort: () =>
|
|
252
|
-
const address = server?.address();
|
|
253
|
-
return address && typeof address === "object" ? address.port : 0;
|
|
254
|
-
},
|
|
217
|
+
getPort: () => actualPort,
|
|
255
218
|
};
|
|
256
219
|
}
|
|
257
220
|
|
|
258
221
|
/**
|
|
259
222
|
* Get the actual port the server is listening on
|
|
260
|
-
* @param server -
|
|
223
|
+
* @param server - Bun server instance
|
|
261
224
|
* @returns Port number
|
|
262
225
|
*/
|
|
263
|
-
export function getServerPort(server:
|
|
264
|
-
|
|
265
|
-
return address && typeof address === "object" ? address.port : 0;
|
|
226
|
+
export function getServerPort(server: ReturnType<typeof Bun.serve>): number {
|
|
227
|
+
return server.port;
|
|
266
228
|
}
|
|
267
|
-
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import * as https from "https";
|
|
2
|
+
import { URL } from "url";
|
|
3
|
+
import { normalizeBaseUrl } from "./util";
|
|
4
|
+
|
|
5
|
+
export class RpcError extends Error {
|
|
6
|
+
rpcName: string;
|
|
7
|
+
statusCode: number;
|
|
8
|
+
payloadText: string;
|
|
9
|
+
payloadJson: any | null;
|
|
10
|
+
|
|
11
|
+
constructor(params: { rpcName: string; statusCode: number; payloadText: string; payloadJson: any | null }) {
|
|
12
|
+
const { rpcName, statusCode, payloadText, payloadJson } = params;
|
|
13
|
+
super(`RPC ${rpcName} failed: HTTP ${statusCode}`);
|
|
14
|
+
this.name = "RpcError";
|
|
15
|
+
this.rpcName = rpcName;
|
|
16
|
+
this.statusCode = statusCode;
|
|
17
|
+
this.payloadText = payloadText;
|
|
18
|
+
this.payloadJson = payloadJson;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function formatRpcErrorForDisplay(err: RpcError): string[] {
|
|
23
|
+
const lines: string[] = [];
|
|
24
|
+
lines.push(`Error: RPC ${err.rpcName} failed: HTTP ${err.statusCode}`);
|
|
25
|
+
|
|
26
|
+
const obj = err.payloadJson && typeof err.payloadJson === "object" ? err.payloadJson : null;
|
|
27
|
+
const details = obj && typeof (obj as any).details === "string" ? (obj as any).details : "";
|
|
28
|
+
const hint = obj && typeof (obj as any).hint === "string" ? (obj as any).hint : "";
|
|
29
|
+
const message = obj && typeof (obj as any).message === "string" ? (obj as any).message : "";
|
|
30
|
+
|
|
31
|
+
if (message) lines.push(`Message: ${message}`);
|
|
32
|
+
if (details) lines.push(`Details: ${details}`);
|
|
33
|
+
if (hint) lines.push(`Hint: ${hint}`);
|
|
34
|
+
|
|
35
|
+
// Fallback to raw payload if we couldn't extract anything useful.
|
|
36
|
+
if (!message && !details && !hint) {
|
|
37
|
+
const t = (err.payloadText || "").trim();
|
|
38
|
+
if (t) lines.push(t);
|
|
39
|
+
}
|
|
40
|
+
return lines;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function unwrapRpcResponse(parsed: unknown): any {
|
|
44
|
+
// Some deployments return a plain object, others return an array of rows,
|
|
45
|
+
// and some wrap OUT params under a "result" key.
|
|
46
|
+
if (Array.isArray(parsed)) {
|
|
47
|
+
if (parsed.length === 1) return unwrapRpcResponse(parsed[0]);
|
|
48
|
+
return parsed;
|
|
49
|
+
}
|
|
50
|
+
if (parsed && typeof parsed === "object") {
|
|
51
|
+
const obj = parsed as any;
|
|
52
|
+
if (obj.result !== undefined) return obj.result;
|
|
53
|
+
}
|
|
54
|
+
return parsed as any;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function postRpc<T>(params: {
|
|
58
|
+
apiKey: string;
|
|
59
|
+
apiBaseUrl: string;
|
|
60
|
+
rpcName: string;
|
|
61
|
+
bodyObj: Record<string, unknown>;
|
|
62
|
+
}): Promise<T> {
|
|
63
|
+
const { apiKey, apiBaseUrl, rpcName, bodyObj } = params;
|
|
64
|
+
if (!apiKey) throw new Error("API key is required");
|
|
65
|
+
const base = normalizeBaseUrl(apiBaseUrl);
|
|
66
|
+
const url = new URL(`${base}/rpc/${rpcName}`);
|
|
67
|
+
const body = JSON.stringify(bodyObj);
|
|
68
|
+
|
|
69
|
+
const headers: Record<string, string> = {
|
|
70
|
+
// The backend RPC functions accept access_token in body, but we also set the header
|
|
71
|
+
// for compatibility with other endpoints and deployments.
|
|
72
|
+
"access-token": apiKey,
|
|
73
|
+
"Prefer": "return=representation",
|
|
74
|
+
"Content-Type": "application/json",
|
|
75
|
+
"Content-Length": Buffer.byteLength(body).toString(),
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return new Promise((resolve, reject) => {
|
|
79
|
+
const req = https.request(
|
|
80
|
+
url,
|
|
81
|
+
{
|
|
82
|
+
method: "POST",
|
|
83
|
+
headers,
|
|
84
|
+
},
|
|
85
|
+
(res) => {
|
|
86
|
+
let data = "";
|
|
87
|
+
res.on("data", (chunk) => (data += chunk));
|
|
88
|
+
res.on("end", () => {
|
|
89
|
+
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
|
90
|
+
try {
|
|
91
|
+
const parsed = JSON.parse(data);
|
|
92
|
+
resolve(unwrapRpcResponse(parsed) as T);
|
|
93
|
+
} catch {
|
|
94
|
+
reject(new Error(`Failed to parse RPC response: ${data}`));
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
const statusCode = res.statusCode || 0;
|
|
98
|
+
let payloadJson: any | null = null;
|
|
99
|
+
if (data) {
|
|
100
|
+
try {
|
|
101
|
+
payloadJson = JSON.parse(data);
|
|
102
|
+
} catch {
|
|
103
|
+
payloadJson = null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
reject(new RpcError({ rpcName, statusCode, payloadText: data, payloadJson }));
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
);
|
|
111
|
+
req.on("error", (err: Error) => reject(err));
|
|
112
|
+
req.write(body);
|
|
113
|
+
req.end();
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function createCheckupReport(params: {
|
|
118
|
+
apiKey: string;
|
|
119
|
+
apiBaseUrl: string;
|
|
120
|
+
project: string;
|
|
121
|
+
status?: string;
|
|
122
|
+
}): Promise<{ reportId: number }> {
|
|
123
|
+
const { apiKey, apiBaseUrl, project, status } = params;
|
|
124
|
+
const bodyObj: Record<string, unknown> = {
|
|
125
|
+
access_token: apiKey,
|
|
126
|
+
project,
|
|
127
|
+
};
|
|
128
|
+
if (status) bodyObj.status = status;
|
|
129
|
+
|
|
130
|
+
const resp = await postRpc<any>({
|
|
131
|
+
apiKey,
|
|
132
|
+
apiBaseUrl,
|
|
133
|
+
rpcName: "checkup_report_create",
|
|
134
|
+
bodyObj,
|
|
135
|
+
});
|
|
136
|
+
const reportId = Number(resp?.report_id);
|
|
137
|
+
if (!Number.isFinite(reportId) || reportId <= 0) {
|
|
138
|
+
throw new Error(`Unexpected checkup_report_create response: ${JSON.stringify(resp)}`);
|
|
139
|
+
}
|
|
140
|
+
return { reportId };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export async function uploadCheckupReportJson(params: {
|
|
144
|
+
apiKey: string;
|
|
145
|
+
apiBaseUrl: string;
|
|
146
|
+
reportId: number;
|
|
147
|
+
filename: string;
|
|
148
|
+
checkId: string;
|
|
149
|
+
jsonText: string;
|
|
150
|
+
}): Promise<{ reportChunkId: number }> {
|
|
151
|
+
const { apiKey, apiBaseUrl, reportId, filename, checkId, jsonText } = params;
|
|
152
|
+
const bodyObj: Record<string, unknown> = {
|
|
153
|
+
access_token: apiKey,
|
|
154
|
+
checkup_report_id: reportId,
|
|
155
|
+
filename,
|
|
156
|
+
check_id: checkId,
|
|
157
|
+
data: jsonText,
|
|
158
|
+
type: "json",
|
|
159
|
+
generate_issue: true,
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const resp = await postRpc<any>({
|
|
163
|
+
apiKey,
|
|
164
|
+
apiBaseUrl,
|
|
165
|
+
rpcName: "checkup_report_file_post",
|
|
166
|
+
bodyObj,
|
|
167
|
+
});
|
|
168
|
+
const chunkId = Number(resp?.report_chunck_id ?? resp?.report_chunk_id);
|
|
169
|
+
if (!Number.isFinite(chunkId) || chunkId <= 0) {
|
|
170
|
+
throw new Error(`Unexpected checkup_report_file_post response: ${JSON.stringify(resp)}`);
|
|
171
|
+
}
|
|
172
|
+
return { reportChunkId: chunkId };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
|