postgresai 0.11.0-alpha.7 → 0.11.0-alpha.9
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/README.md +81 -4
- package/bin/postgres-ai.ts +1003 -0
- package/dist/bin/postgres-ai.d.ts +3 -0
- package/dist/bin/postgres-ai.d.ts.map +1 -0
- package/dist/bin/postgres-ai.js +897 -0
- package/dist/bin/postgres-ai.js.map +1 -0
- package/dist/lib/auth-server.d.ts +31 -0
- package/dist/lib/auth-server.d.ts.map +1 -0
- package/dist/lib/auth-server.js +263 -0
- package/dist/lib/auth-server.js.map +1 -0
- package/dist/lib/config.d.ts +45 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +181 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/pkce.d.ts +32 -0
- package/dist/lib/pkce.d.ts.map +1 -0
- package/dist/lib/pkce.js +101 -0
- package/dist/lib/pkce.js.map +1 -0
- package/dist/package.json +42 -0
- package/lib/auth-server.ts +267 -0
- package/lib/config.ts +161 -0
- package/lib/pkce.ts +79 -0
- package/package.json +17 -8
- package/tsconfig.json +28 -0
- package/bin/postgres-ai.js +0 -703
package/dist/lib/pkce.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
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"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "postgresai",
|
|
3
|
+
"version": "0.11.0-alpha.9",
|
|
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
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"commander": "^12.1.0",
|
|
32
|
+
"js-yaml": "^4.1.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/js-yaml": "^4.0.9",
|
|
36
|
+
"@types/node": "^18.19.0",
|
|
37
|
+
"typescript": "^5.3.3"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import * as http from "http";
|
|
2
|
+
import { URL } from "url";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* OAuth callback result
|
|
6
|
+
*/
|
|
7
|
+
export interface CallbackResult {
|
|
8
|
+
code: string;
|
|
9
|
+
state: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Callback server structure
|
|
14
|
+
*/
|
|
15
|
+
export interface CallbackServer {
|
|
16
|
+
server: http.Server;
|
|
17
|
+
promise: Promise<CallbackResult>;
|
|
18
|
+
getPort: () => number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Simple HTML escape utility
|
|
23
|
+
* @param str - String to escape
|
|
24
|
+
* @returns Escaped string
|
|
25
|
+
*/
|
|
26
|
+
function escapeHtml(str: string | null): string {
|
|
27
|
+
if (!str) return "";
|
|
28
|
+
return String(str)
|
|
29
|
+
.replace(/&/g, "&")
|
|
30
|
+
.replace(/</g, "<")
|
|
31
|
+
.replace(/>/g, ">")
|
|
32
|
+
.replace(/"/g, """)
|
|
33
|
+
.replace(/'/g, "'");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Create and start callback server, returning server object and promise
|
|
38
|
+
* @param port - Port to listen on (0 for random available port)
|
|
39
|
+
* @param expectedState - Expected state parameter for CSRF protection
|
|
40
|
+
* @param timeoutMs - Timeout in milliseconds
|
|
41
|
+
* @returns Server object with promise and getPort function
|
|
42
|
+
*/
|
|
43
|
+
export function createCallbackServer(
|
|
44
|
+
port: number = 0,
|
|
45
|
+
expectedState: string | null = null,
|
|
46
|
+
timeoutMs: number = 300000
|
|
47
|
+
): CallbackServer {
|
|
48
|
+
let resolved = false;
|
|
49
|
+
let server: http.Server | null = null;
|
|
50
|
+
let actualPort = port;
|
|
51
|
+
let resolveCallback: (value: CallbackResult) => void;
|
|
52
|
+
let rejectCallback: (reason: Error) => void;
|
|
53
|
+
|
|
54
|
+
const promise = new Promise<CallbackResult>((resolve, reject) => {
|
|
55
|
+
resolveCallback = resolve;
|
|
56
|
+
rejectCallback = reject;
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Timeout handler
|
|
60
|
+
const timeout = setTimeout(() => {
|
|
61
|
+
if (!resolved) {
|
|
62
|
+
resolved = true;
|
|
63
|
+
if (server) {
|
|
64
|
+
server.close();
|
|
65
|
+
}
|
|
66
|
+
rejectCallback(new Error("Authentication timeout. Please try again."));
|
|
67
|
+
}
|
|
68
|
+
}, timeoutMs);
|
|
69
|
+
|
|
70
|
+
// Request handler
|
|
71
|
+
const requestHandler = (req: http.IncomingMessage, res: http.ServerResponse): void => {
|
|
72
|
+
if (resolved) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
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
|
+
}
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const url = new URL(req.url, `http://localhost:${actualPort}`);
|
|
85
|
+
const code = url.searchParams.get("code");
|
|
86
|
+
const state = url.searchParams.get("state");
|
|
87
|
+
const error = url.searchParams.get("error");
|
|
88
|
+
const errorDescription = url.searchParams.get("error_description");
|
|
89
|
+
|
|
90
|
+
// Handle OAuth error
|
|
91
|
+
if (error) {
|
|
92
|
+
resolved = true;
|
|
93
|
+
clearTimeout(timeout);
|
|
94
|
+
|
|
95
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
96
|
+
res.end(`
|
|
97
|
+
<!DOCTYPE html>
|
|
98
|
+
<html>
|
|
99
|
+
<head>
|
|
100
|
+
<title>Authentication Failed</title>
|
|
101
|
+
<style>
|
|
102
|
+
body { font-family: system-ui, -apple-system, sans-serif; max-width: 600px; margin: 50px auto; padding: 20px; }
|
|
103
|
+
.error { background: #fee; border: 1px solid #fcc; padding: 20px; border-radius: 8px; }
|
|
104
|
+
h1 { color: #c33; margin-top: 0; }
|
|
105
|
+
code { background: #f5f5f5; padding: 2px 6px; border-radius: 3px; }
|
|
106
|
+
</style>
|
|
107
|
+
</head>
|
|
108
|
+
<body>
|
|
109
|
+
<div class="error">
|
|
110
|
+
<h1>Authentication Failed</h1>
|
|
111
|
+
<p><strong>Error:</strong> ${escapeHtml(error)}</p>
|
|
112
|
+
${errorDescription ? `<p><strong>Description:</strong> ${escapeHtml(errorDescription)}</p>` : ""}
|
|
113
|
+
<p>You can close this window and return to your terminal.</p>
|
|
114
|
+
</div>
|
|
115
|
+
</body>
|
|
116
|
+
</html>
|
|
117
|
+
`);
|
|
118
|
+
|
|
119
|
+
if (server) {
|
|
120
|
+
server.close();
|
|
121
|
+
}
|
|
122
|
+
rejectCallback(new Error(`OAuth error: ${error}${errorDescription ? ` - ${errorDescription}` : ""}`));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Validate required parameters
|
|
127
|
+
if (!code || !state) {
|
|
128
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
129
|
+
res.end(`
|
|
130
|
+
<!DOCTYPE html>
|
|
131
|
+
<html>
|
|
132
|
+
<head>
|
|
133
|
+
<title>Authentication Failed</title>
|
|
134
|
+
<style>
|
|
135
|
+
body { font-family: system-ui, -apple-system, sans-serif; max-width: 600px; margin: 50px auto; padding: 20px; }
|
|
136
|
+
.error { background: #fee; border: 1px solid #fcc; padding: 20px; border-radius: 8px; }
|
|
137
|
+
h1 { color: #c33; margin-top: 0; }
|
|
138
|
+
</style>
|
|
139
|
+
</head>
|
|
140
|
+
<body>
|
|
141
|
+
<div class="error">
|
|
142
|
+
<h1>Authentication Failed</h1>
|
|
143
|
+
<p>Missing required parameters (code or state).</p>
|
|
144
|
+
<p>You can close this window and return to your terminal.</p>
|
|
145
|
+
</div>
|
|
146
|
+
</body>
|
|
147
|
+
</html>
|
|
148
|
+
`);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Validate state (CSRF protection)
|
|
153
|
+
if (expectedState && state !== expectedState) {
|
|
154
|
+
resolved = true;
|
|
155
|
+
clearTimeout(timeout);
|
|
156
|
+
|
|
157
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
158
|
+
res.end(`
|
|
159
|
+
<!DOCTYPE html>
|
|
160
|
+
<html>
|
|
161
|
+
<head>
|
|
162
|
+
<title>Authentication Failed</title>
|
|
163
|
+
<style>
|
|
164
|
+
body { font-family: system-ui, -apple-system, sans-serif; max-width: 600px; margin: 50px auto; padding: 20px; }
|
|
165
|
+
.error { background: #fee; border: 1px solid #fcc; padding: 20px; border-radius: 8px; }
|
|
166
|
+
h1 { color: #c33; margin-top: 0; }
|
|
167
|
+
</style>
|
|
168
|
+
</head>
|
|
169
|
+
<body>
|
|
170
|
+
<div class="error">
|
|
171
|
+
<h1>Authentication Failed</h1>
|
|
172
|
+
<p>Invalid state parameter (possible CSRF attack).</p>
|
|
173
|
+
<p>You can close this window and return to your terminal.</p>
|
|
174
|
+
</div>
|
|
175
|
+
</body>
|
|
176
|
+
</html>
|
|
177
|
+
`);
|
|
178
|
+
|
|
179
|
+
if (server) {
|
|
180
|
+
server.close();
|
|
181
|
+
}
|
|
182
|
+
rejectCallback(new Error("State mismatch (possible CSRF attack)"));
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Success!
|
|
187
|
+
resolved = true;
|
|
188
|
+
clearTimeout(timeout);
|
|
189
|
+
|
|
190
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
191
|
+
res.end(`
|
|
192
|
+
<!DOCTYPE html>
|
|
193
|
+
<html>
|
|
194
|
+
<head>
|
|
195
|
+
<title>Authentication Successful</title>
|
|
196
|
+
<style>
|
|
197
|
+
body { font-family: system-ui, -apple-system, sans-serif; max-width: 600px; margin: 50px auto; padding: 20px; }
|
|
198
|
+
.success { background: #efe; border: 1px solid #cfc; padding: 20px; border-radius: 8px; }
|
|
199
|
+
h1 { color: #3c3; margin-top: 0; }
|
|
200
|
+
</style>
|
|
201
|
+
</head>
|
|
202
|
+
<body>
|
|
203
|
+
<div class="success">
|
|
204
|
+
<h1>Authentication Successful</h1>
|
|
205
|
+
<p>You have successfully authenticated the PostgresAI CLI.</p>
|
|
206
|
+
<p>You can close this window and return to your terminal.</p>
|
|
207
|
+
</div>
|
|
208
|
+
</body>
|
|
209
|
+
</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
|
+
}
|
|
239
|
+
});
|
|
240
|
+
|
|
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
|
+
|
|
248
|
+
return {
|
|
249
|
+
server,
|
|
250
|
+
promise,
|
|
251
|
+
getPort: () => {
|
|
252
|
+
const address = server?.address();
|
|
253
|
+
return address && typeof address === "object" ? address.port : 0;
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Get the actual port the server is listening on
|
|
260
|
+
* @param server - HTTP server instance
|
|
261
|
+
* @returns Port number
|
|
262
|
+
*/
|
|
263
|
+
export function getServerPort(server: http.Server): number {
|
|
264
|
+
const address = server.address();
|
|
265
|
+
return address && typeof address === "object" ? address.port : 0;
|
|
266
|
+
}
|
|
267
|
+
|
package/lib/config.ts
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import * as os from "os";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Configuration object structure
|
|
7
|
+
*/
|
|
8
|
+
export interface Config {
|
|
9
|
+
apiKey: string | null;
|
|
10
|
+
baseUrl: string | null;
|
|
11
|
+
orgId: number | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get the user-level config directory path
|
|
16
|
+
* @returns Path to ~/.config/postgresai
|
|
17
|
+
*/
|
|
18
|
+
export function getConfigDir(): string {
|
|
19
|
+
const configHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
|
|
20
|
+
return path.join(configHome, "postgresai");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get the user-level config file path
|
|
25
|
+
* @returns Path to ~/.config/postgresai/config.json
|
|
26
|
+
*/
|
|
27
|
+
export function getConfigPath(): string {
|
|
28
|
+
return path.join(getConfigDir(), "config.json");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get the legacy project-local config file path
|
|
33
|
+
* @returns Path to .pgwatch-config in current directory
|
|
34
|
+
*/
|
|
35
|
+
export function getLegacyConfigPath(): string {
|
|
36
|
+
return path.resolve(process.cwd(), ".pgwatch-config");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Read configuration from file
|
|
41
|
+
* Tries user-level config first, then falls back to legacy project-local config
|
|
42
|
+
* @returns Configuration object with apiKey, baseUrl, orgId
|
|
43
|
+
*/
|
|
44
|
+
export function readConfig(): Config {
|
|
45
|
+
const config: Config = {
|
|
46
|
+
apiKey: null,
|
|
47
|
+
baseUrl: null,
|
|
48
|
+
orgId: null,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Try user-level config first
|
|
52
|
+
const userConfigPath = getConfigPath();
|
|
53
|
+
if (fs.existsSync(userConfigPath)) {
|
|
54
|
+
try {
|
|
55
|
+
const content = fs.readFileSync(userConfigPath, "utf8");
|
|
56
|
+
const parsed = JSON.parse(content);
|
|
57
|
+
config.apiKey = parsed.apiKey || null;
|
|
58
|
+
config.baseUrl = parsed.baseUrl || null;
|
|
59
|
+
config.orgId = parsed.orgId || null;
|
|
60
|
+
return config;
|
|
61
|
+
} catch (err) {
|
|
62
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
63
|
+
console.error(`Warning: Failed to read config from ${userConfigPath}: ${message}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Fall back to legacy project-local config
|
|
68
|
+
const legacyPath = getLegacyConfigPath();
|
|
69
|
+
if (fs.existsSync(legacyPath)) {
|
|
70
|
+
try {
|
|
71
|
+
const stats = fs.statSync(legacyPath);
|
|
72
|
+
if (stats.isFile()) {
|
|
73
|
+
const content = fs.readFileSync(legacyPath, "utf8");
|
|
74
|
+
const lines = content.split(/\r?\n/);
|
|
75
|
+
for (const line of lines) {
|
|
76
|
+
const match = line.match(/^api_key=(.+)$/);
|
|
77
|
+
if (match) {
|
|
78
|
+
config.apiKey = match[1].trim();
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
} catch (err) {
|
|
84
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
85
|
+
console.error(`Warning: Failed to read legacy config from ${legacyPath}: ${message}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return config;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Write configuration to user-level config file
|
|
94
|
+
* @param config - Configuration object with apiKey, baseUrl, orgId
|
|
95
|
+
*/
|
|
96
|
+
export function writeConfig(config: Partial<Config>): void {
|
|
97
|
+
const configDir = getConfigDir();
|
|
98
|
+
const configPath = getConfigPath();
|
|
99
|
+
|
|
100
|
+
// Ensure config directory exists
|
|
101
|
+
if (!fs.existsSync(configDir)) {
|
|
102
|
+
fs.mkdirSync(configDir, { recursive: true, mode: 0o700 });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Read existing config and merge
|
|
106
|
+
let existingConfig: Record<string, unknown> = {};
|
|
107
|
+
if (fs.existsSync(configPath)) {
|
|
108
|
+
try {
|
|
109
|
+
const content = fs.readFileSync(configPath, "utf8");
|
|
110
|
+
existingConfig = JSON.parse(content);
|
|
111
|
+
} catch (err) {
|
|
112
|
+
// Ignore parse errors, will overwrite
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const mergedConfig = {
|
|
117
|
+
...existingConfig,
|
|
118
|
+
...config,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// Write config file with restricted permissions
|
|
122
|
+
fs.writeFileSync(configPath, JSON.stringify(mergedConfig, null, 2) + "\n", {
|
|
123
|
+
mode: 0o600,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Delete specific keys from configuration
|
|
129
|
+
* @param keys - Array of keys to delete (e.g., ['apiKey'])
|
|
130
|
+
*/
|
|
131
|
+
export function deleteConfigKeys(keys: string[]): void {
|
|
132
|
+
const configPath = getConfigPath();
|
|
133
|
+
if (!fs.existsSync(configPath)) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const content = fs.readFileSync(configPath, "utf8");
|
|
139
|
+
const config: Record<string, unknown> = JSON.parse(content);
|
|
140
|
+
|
|
141
|
+
for (const key of keys) {
|
|
142
|
+
delete config[key];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", {
|
|
146
|
+
mode: 0o600,
|
|
147
|
+
});
|
|
148
|
+
} catch (err) {
|
|
149
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
150
|
+
console.error(`Warning: Failed to update config: ${message}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Check if config file exists
|
|
156
|
+
* @returns True if config exists
|
|
157
|
+
*/
|
|
158
|
+
export function configExists(): boolean {
|
|
159
|
+
return fs.existsSync(getConfigPath()) || fs.existsSync(getLegacyConfigPath());
|
|
160
|
+
}
|
|
161
|
+
|
package/lib/pkce.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import * as crypto from "crypto";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* PKCE parameters for OAuth 2.0 Authorization Code Flow with PKCE
|
|
5
|
+
*/
|
|
6
|
+
export interface PKCEParams {
|
|
7
|
+
codeVerifier: string;
|
|
8
|
+
codeChallenge: string;
|
|
9
|
+
codeChallengeMethod: "S256";
|
|
10
|
+
state: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Generate a cryptographically random string for PKCE
|
|
15
|
+
* @param length - Length of the string (43-128 characters per RFC 7636)
|
|
16
|
+
* @returns Base64URL-encoded random string
|
|
17
|
+
*/
|
|
18
|
+
function generateRandomString(length: number = 64): string {
|
|
19
|
+
const bytes = crypto.randomBytes(length);
|
|
20
|
+
return base64URLEncode(bytes);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Base64URL encode (without padding)
|
|
25
|
+
* @param buffer - Buffer to encode
|
|
26
|
+
* @returns Base64URL-encoded string
|
|
27
|
+
*/
|
|
28
|
+
function base64URLEncode(buffer: Buffer): string {
|
|
29
|
+
return buffer
|
|
30
|
+
.toString("base64")
|
|
31
|
+
.replace(/\+/g, "-")
|
|
32
|
+
.replace(/\//g, "_")
|
|
33
|
+
.replace(/=/g, "");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Generate PKCE code verifier
|
|
38
|
+
* @returns Random code verifier (43-128 characters)
|
|
39
|
+
*/
|
|
40
|
+
export function generateCodeVerifier(): string {
|
|
41
|
+
return generateRandomString(32); // 32 bytes = 43 chars after base64url encoding
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Generate PKCE code challenge from verifier
|
|
46
|
+
* Uses S256 method (SHA256)
|
|
47
|
+
* @param verifier - Code verifier string
|
|
48
|
+
* @returns Base64URL-encoded SHA256 hash of verifier
|
|
49
|
+
*/
|
|
50
|
+
export function generateCodeChallenge(verifier: string): string {
|
|
51
|
+
const hash = crypto.createHash("sha256").update(verifier).digest();
|
|
52
|
+
return base64URLEncode(hash);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Generate random state for CSRF protection
|
|
57
|
+
* @returns Random state string
|
|
58
|
+
*/
|
|
59
|
+
export function generateState(): string {
|
|
60
|
+
return generateRandomString(16); // 16 bytes = 22 chars
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Generate complete PKCE parameters
|
|
65
|
+
* @returns Object with verifier, challenge, challengeMethod, and state
|
|
66
|
+
*/
|
|
67
|
+
export function generatePKCEParams(): PKCEParams {
|
|
68
|
+
const verifier = generateCodeVerifier();
|
|
69
|
+
const challenge = generateCodeChallenge(verifier);
|
|
70
|
+
const state = generateState();
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
codeVerifier: verifier,
|
|
74
|
+
codeChallenge: challenge,
|
|
75
|
+
codeChallengeMethod: "S256",
|
|
76
|
+
state: state,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "postgresai",
|
|
3
|
-
"version": "0.11.0-alpha.
|
|
4
|
-
"description": "
|
|
5
|
-
"license": "
|
|
3
|
+
"version": "0.11.0-alpha.9",
|
|
4
|
+
"description": "postgres_ai CLI (Node.js)",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
6
|
"private": false,
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
@@ -13,19 +13,28 @@
|
|
|
13
13
|
"url": "https://gitlab.com/postgres-ai/postgres_ai/-/issues"
|
|
14
14
|
},
|
|
15
15
|
"bin": {
|
|
16
|
-
"postgres-ai": "./bin/postgres-ai.js",
|
|
17
|
-
"postgresai": "./bin/postgres-ai.js",
|
|
18
|
-
"pgai": "./bin/postgres-ai.js"
|
|
16
|
+
"postgres-ai": "./dist/bin/postgres-ai.js",
|
|
17
|
+
"postgresai": "./dist/bin/postgres-ai.js",
|
|
18
|
+
"pgai": "./dist/bin/postgres-ai.js"
|
|
19
19
|
},
|
|
20
20
|
"type": "commonjs",
|
|
21
21
|
"engines": {
|
|
22
22
|
"node": ">=18"
|
|
23
23
|
},
|
|
24
24
|
"scripts": {
|
|
25
|
-
"
|
|
25
|
+
"build": "tsc",
|
|
26
|
+
"prepare": "npm run build",
|
|
27
|
+
"start": "node ./dist/bin/postgres-ai.js --help",
|
|
28
|
+
"dev": "tsc --watch"
|
|
26
29
|
},
|
|
27
30
|
"dependencies": {
|
|
28
|
-
"commander": "^12.1.0"
|
|
31
|
+
"commander": "^12.1.0",
|
|
32
|
+
"js-yaml": "^4.1.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/js-yaml": "^4.0.9",
|
|
36
|
+
"@types/node": "^18.19.0",
|
|
37
|
+
"typescript": "^5.3.3"
|
|
29
38
|
},
|
|
30
39
|
"publishConfig": {
|
|
31
40
|
"access": "public"
|