postgresai 0.14.0-dev.43 → 0.14.0-dev.44

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 (58) hide show
  1. package/bin/postgres-ai.ts +649 -310
  2. package/bun.lock +258 -0
  3. package/dist/bin/postgres-ai.js +29491 -1910
  4. package/dist/sql/01.role.sql +16 -0
  5. package/dist/sql/02.permissions.sql +37 -0
  6. package/dist/sql/03.optional_rds.sql +6 -0
  7. package/dist/sql/04.optional_self_managed.sql +8 -0
  8. package/dist/sql/05.helpers.sql +415 -0
  9. package/lib/auth-server.ts +58 -97
  10. package/lib/checkup-api.ts +175 -0
  11. package/lib/checkup.ts +833 -0
  12. package/lib/config.ts +3 -0
  13. package/lib/init.ts +106 -74
  14. package/lib/issues.ts +121 -194
  15. package/lib/mcp-server.ts +6 -17
  16. package/lib/metrics-loader.ts +156 -0
  17. package/package.json +13 -9
  18. package/sql/02.permissions.sql +9 -5
  19. package/sql/05.helpers.sql +415 -0
  20. package/test/checkup.test.ts +953 -0
  21. package/test/init.integration.test.ts +396 -0
  22. package/test/init.test.ts +345 -0
  23. package/test/schema-validation.test.ts +188 -0
  24. package/tsconfig.json +12 -20
  25. package/dist/bin/postgres-ai.d.ts +0 -3
  26. package/dist/bin/postgres-ai.d.ts.map +0 -1
  27. package/dist/bin/postgres-ai.js.map +0 -1
  28. package/dist/lib/auth-server.d.ts +0 -31
  29. package/dist/lib/auth-server.d.ts.map +0 -1
  30. package/dist/lib/auth-server.js +0 -263
  31. package/dist/lib/auth-server.js.map +0 -1
  32. package/dist/lib/config.d.ts +0 -45
  33. package/dist/lib/config.d.ts.map +0 -1
  34. package/dist/lib/config.js +0 -181
  35. package/dist/lib/config.js.map +0 -1
  36. package/dist/lib/init.d.ts +0 -85
  37. package/dist/lib/init.d.ts.map +0 -1
  38. package/dist/lib/init.js +0 -644
  39. package/dist/lib/init.js.map +0 -1
  40. package/dist/lib/issues.d.ts +0 -75
  41. package/dist/lib/issues.d.ts.map +0 -1
  42. package/dist/lib/issues.js +0 -336
  43. package/dist/lib/issues.js.map +0 -1
  44. package/dist/lib/mcp-server.d.ts +0 -9
  45. package/dist/lib/mcp-server.d.ts.map +0 -1
  46. package/dist/lib/mcp-server.js +0 -168
  47. package/dist/lib/mcp-server.js.map +0 -1
  48. package/dist/lib/pkce.d.ts +0 -32
  49. package/dist/lib/pkce.d.ts.map +0 -1
  50. package/dist/lib/pkce.js +0 -101
  51. package/dist/lib/pkce.js.map +0 -1
  52. package/dist/lib/util.d.ts +0 -27
  53. package/dist/lib/util.d.ts.map +0 -1
  54. package/dist/lib/util.js +0 -46
  55. package/dist/lib/util.js.map +0 -1
  56. package/dist/package.json +0 -46
  57. package/test/init.integration.test.cjs +0 -382
  58. package/test/init.test.cjs +0 -392
@@ -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: http.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, returning server object and promise
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 (server) {
64
- server.close();
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
- // Only handle /callback path
77
- if (!req.url || !req.url.startsWith("/callback")) {
78
- res.writeHead(404, { "Content-Type": "text/plain" });
79
- res.end("Not Found");
80
- return;
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
- res.writeHead(400, { "Content-Type": "text/html" });
96
- res.end(`
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
- res.writeHead(400, { "Content-Type": "text/html" });
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
- res.writeHead(400, { "Content-Type": "text/html" });
158
- res.end(`
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
- res.writeHead(200, { "Content-Type": "text/html" });
191
- res.end(`
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
- server.listen(port, "127.0.0.1", () => {
242
- const address = server?.address();
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 - HTTP server instance
223
+ * @param server - Bun server instance
261
224
  * @returns Port number
262
225
  */
263
- export function getServerPort(server: http.Server): number {
264
- const address = server.address();
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
+