postgresai 0.14.0-dev.51 → 0.14.0-dev.52

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.
@@ -1,3 +1,5 @@
1
+ import * as http from "http";
2
+
1
3
  /**
2
4
  * OAuth callback result
3
5
  */
@@ -31,7 +33,7 @@ function escapeHtml(str: string | null): string {
31
33
  }
32
34
 
33
35
  /**
34
- * Create and start callback server using Bun.serve
36
+ * Create and start callback server using Node.js http module
35
37
  *
36
38
  * @param port - Port to listen on (0 for random available port)
37
39
  * @param expectedState - Expected state parameter for CSRF protection
@@ -53,53 +55,60 @@ export function createCallbackServer(
53
55
  let actualPort = port;
54
56
  let resolveCallback: (value: CallbackResult) => void;
55
57
  let rejectCallback: (reason: Error) => void;
56
- let serverInstance: ReturnType<typeof Bun.serve> | null = null;
58
+ let serverInstance: http.Server | null = null;
57
59
 
58
60
  const promise = new Promise<CallbackResult>((resolve, reject) => {
59
61
  resolveCallback = resolve;
60
62
  rejectCallback = reject;
61
63
  });
62
64
 
65
+ const stopServer = () => {
66
+ if (serverInstance) {
67
+ serverInstance.close();
68
+ serverInstance = null;
69
+ }
70
+ };
71
+
63
72
  // Timeout handler
64
73
  const timeout = setTimeout(() => {
65
74
  if (!resolved) {
66
75
  resolved = true;
67
- if (serverInstance) {
68
- serverInstance.stop();
69
- }
76
+ stopServer();
70
77
  rejectCallback(new Error("Authentication timeout. Please try again."));
71
78
  }
72
79
  }, timeoutMs);
73
80
 
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
+ serverInstance = http.createServer((req, res) => {
82
+ if (resolved) {
83
+ res.writeHead(200, { "Content-Type": "text/plain" });
84
+ res.end("Already handled");
85
+ return;
86
+ }
81
87
 
82
- const url = new URL(req.url);
88
+ const url = new URL(req.url || "/", `http://127.0.0.1:${actualPort}`);
83
89
 
84
- // Only handle /callback path
85
- if (!url.pathname.startsWith("/callback")) {
86
- return new Response("Not Found", { status: 404 });
87
- }
90
+ // Only handle /callback path
91
+ if (!url.pathname.startsWith("/callback")) {
92
+ res.writeHead(404, { "Content-Type": "text/plain" });
93
+ res.end("Not Found");
94
+ return;
95
+ }
88
96
 
89
- const code = url.searchParams.get("code");
90
- const state = url.searchParams.get("state");
91
- const error = url.searchParams.get("error");
92
- const errorDescription = url.searchParams.get("error_description");
97
+ const code = url.searchParams.get("code");
98
+ const state = url.searchParams.get("state");
99
+ const error = url.searchParams.get("error");
100
+ const errorDescription = url.searchParams.get("error_description");
93
101
 
94
- // Handle OAuth error
95
- if (error) {
96
- resolved = true;
97
- clearTimeout(timeout);
102
+ // Handle OAuth error
103
+ if (error) {
104
+ resolved = true;
105
+ clearTimeout(timeout);
98
106
 
99
- setTimeout(() => serverInstance?.stop(), 100);
100
- rejectCallback(new Error(`OAuth error: ${error}${errorDescription ? ` - ${errorDescription}` : ""}`));
107
+ setTimeout(() => stopServer(), 100);
108
+ rejectCallback(new Error(`OAuth error: ${error}${errorDescription ? ` - ${errorDescription}` : ""}`));
101
109
 
102
- return new Response(`
110
+ res.writeHead(400, { "Content-Type": "text/html" });
111
+ res.end(`
103
112
  <!DOCTYPE html>
104
113
  <html>
105
114
  <head>
@@ -120,12 +129,14 @@ export function createCallbackServer(
120
129
  </div>
121
130
  </body>
122
131
  </html>
123
- `, { status: 400, headers: { "Content-Type": "text/html" } });
124
- }
132
+ `);
133
+ return;
134
+ }
125
135
 
126
- // Validate required parameters
127
- if (!code || !state) {
128
- return new Response(`
136
+ // Validate required parameters
137
+ if (!code || !state) {
138
+ res.writeHead(400, { "Content-Type": "text/html" });
139
+ res.end(`
129
140
  <!DOCTYPE html>
130
141
  <html>
131
142
  <head>
@@ -144,18 +155,20 @@ export function createCallbackServer(
144
155
  </div>
145
156
  </body>
146
157
  </html>
147
- `, { status: 400, headers: { "Content-Type": "text/html" } });
148
- }
158
+ `);
159
+ return;
160
+ }
149
161
 
150
- // Validate state (CSRF protection)
151
- if (expectedState && state !== expectedState) {
152
- resolved = true;
153
- clearTimeout(timeout);
162
+ // Validate state (CSRF protection)
163
+ if (expectedState && state !== expectedState) {
164
+ resolved = true;
165
+ clearTimeout(timeout);
154
166
 
155
- setTimeout(() => serverInstance?.stop(), 100);
156
- rejectCallback(new Error("State mismatch (possible CSRF attack)"));
167
+ setTimeout(() => stopServer(), 100);
168
+ rejectCallback(new Error("State mismatch (possible CSRF attack)"));
157
169
 
158
- return new Response(`
170
+ res.writeHead(400, { "Content-Type": "text/html" });
171
+ res.end(`
159
172
  <!DOCTYPE html>
160
173
  <html>
161
174
  <head>
@@ -174,19 +187,21 @@ export function createCallbackServer(
174
187
  </div>
175
188
  </body>
176
189
  </html>
177
- `, { status: 400, headers: { "Content-Type": "text/html" } });
178
- }
190
+ `);
191
+ return;
192
+ }
179
193
 
180
- // Success!
181
- resolved = true;
182
- clearTimeout(timeout);
194
+ // Success!
195
+ resolved = true;
196
+ clearTimeout(timeout);
183
197
 
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);
198
+ // Resolve first, then stop server asynchronously after response is sent.
199
+ // The 100ms delay ensures the HTTP response is fully written before closing.
200
+ resolveCallback({ code, state });
201
+ setTimeout(() => stopServer(), 100);
188
202
 
189
- return new Response(`
203
+ res.writeHead(200, { "Content-Type": "text/html" });
204
+ res.end(`
190
205
  <!DOCTYPE html>
191
206
  <html>
192
207
  <head>
@@ -205,24 +220,19 @@ export function createCallbackServer(
205
220
  </div>
206
221
  </body>
207
222
  </html>
208
- `, { status: 200, headers: { "Content-Type": "text/html" } });
209
- },
223
+ `);
210
224
  });
211
225
 
212
- actualPort = serverInstance.port;
226
+ serverInstance.listen(port, "127.0.0.1", () => {
227
+ const address = serverInstance?.address();
228
+ if (address && typeof address === "object") {
229
+ actualPort = address.port;
230
+ }
231
+ });
213
232
 
214
233
  return {
215
- server: { stop: () => serverInstance?.stop() },
234
+ server: { stop: stopServer },
216
235
  promise,
217
236
  getPort: () => actualPort,
218
237
  };
219
238
  }
220
-
221
- /**
222
- * Get the actual port the server is listening on
223
- * @param server - Bun server instance
224
- * @returns Port number
225
- */
226
- export function getServerPort(server: ReturnType<typeof Bun.serve>): number {
227
- return server.port;
228
- }
package/lib/config.ts CHANGED
@@ -9,7 +9,6 @@ export interface Config {
9
9
  apiKey: string | null;
10
10
  baseUrl: string | null;
11
11
  orgId: number | null;
12
- defaultProject: string | null;
13
12
  }
14
13
 
15
14
  /**
@@ -47,7 +46,6 @@ export function readConfig(): Config {
47
46
  apiKey: null,
48
47
  baseUrl: null,
49
48
  orgId: null,
50
- defaultProject: null,
51
49
  };
52
50
 
53
51
  // Try user-level config first
@@ -59,7 +57,6 @@ export function readConfig(): Config {
59
57
  config.apiKey = parsed.apiKey || null;
60
58
  config.baseUrl = parsed.baseUrl || null;
61
59
  config.orgId = parsed.orgId || null;
62
- config.defaultProject = parsed.defaultProject || null;
63
60
  return config;
64
61
  } catch (err) {
65
62
  const message = err instanceof Error ? err.message : String(err);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postgresai",
3
- "version": "0.14.0-dev.51",
3
+ "version": "0.14.0-dev.52",
4
4
  "description": "postgres_ai CLI",
5
5
  "license": "Apache-2.0",
6
6
  "private": false,
@@ -22,7 +22,7 @@
22
22
  "node": ">=18"
23
23
  },
24
24
  "scripts": {
25
- "build": "bun build ./bin/postgres-ai.ts --outdir ./dist/bin --target node && node -e \"const fs=require('fs');const f='./dist/bin/postgres-ai.js';fs.writeFileSync(f,fs.readFileSync(f,'utf8').replace('#!/usr/bin/env bun','#!/usr/bin/env node'))\"",
25
+ "build": "bun build ./bin/postgres-ai.ts --outdir ./dist/bin --target node && node -e \"const fs=require('fs');const f='./dist/bin/postgres-ai.js';fs.writeFileSync(f,fs.readFileSync(f,'utf8').replace('#!/usr/bin/env bun','#!/usr/bin/env node'))\" && cp -r sql dist/",
26
26
  "prepublishOnly": "npm run build",
27
27
  "start": "bun ./bin/postgres-ai.ts --help",
28
28
  "start:node": "node ./dist/bin/postgres-ai.js --help",
@@ -40,8 +40,6 @@
40
40
  "@types/bun": "^1.1.14",
41
41
  "@types/js-yaml": "^4.0.9",
42
42
  "@types/pg": "^8.15.6",
43
- "ajv": "^8.17.1",
44
- "ajv-formats": "^3.0.1",
45
43
  "typescript": "^5.3.3"
46
44
  },
47
45
  "publishConfig": {
@@ -213,7 +213,7 @@ describe.skipIf(skipTests)("integration: prepare-db", () => {
213
213
  } finally {
214
214
  await pg.cleanup();
215
215
  }
216
- }, { timeout: 30000 });
216
+ });
217
217
 
218
218
  test("requires explicit monitoring password in non-interactive mode", async () => {
219
219
  pg = await createTempPostgres();
@@ -237,7 +237,7 @@ describe.skipIf(skipTests)("integration: prepare-db", () => {
237
237
  } finally {
238
238
  await pg.cleanup();
239
239
  }
240
- }, { timeout: 30000 });
240
+ });
241
241
 
242
242
  test("fixes slightly-off permissions idempotently", async () => {
243
243
  pg = await createTempPostgres();
@@ -291,7 +291,7 @@ describe.skipIf(skipTests)("integration: prepare-db", () => {
291
291
  } finally {
292
292
  await pg.cleanup();
293
293
  }
294
- }, { timeout: 30000 });
294
+ });
295
295
 
296
296
  test("reports nicely when lacking permissions", async () => {
297
297
  pg = await createTempPostgres();
@@ -324,7 +324,7 @@ describe.skipIf(skipTests)("integration: prepare-db", () => {
324
324
  } finally {
325
325
  await pg.cleanup();
326
326
  }
327
- }, { timeout: 30000 });
327
+ });
328
328
 
329
329
  test("--verify returns 0 when ok and non-zero when missing", async () => {
330
330
  pg = await createTempPostgres();
@@ -360,7 +360,7 @@ describe.skipIf(skipTests)("integration: prepare-db", () => {
360
360
  } finally {
361
361
  await pg.cleanup();
362
362
  }
363
- }, { timeout: 30000 });
363
+ });
364
364
 
365
365
  test("--reset-password updates the monitoring role login password", async () => {
366
366
  pg = await createTempPostgres();
@@ -392,5 +392,5 @@ describe.skipIf(skipTests)("integration: prepare-db", () => {
392
392
  } finally {
393
393
  await pg.cleanup();
394
394
  }
395
- }, { timeout: 30000 });
395
+ });
396
396
  });
@@ -1,175 +0,0 @@
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
-