neonctl 1.29.3 → 1.29.5
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/auth.js +16 -2
- package/commands/auth.test.js +1 -1
- package/commands/connection_string.js +11 -9
- package/commands/connection_string.test.js +18 -1
- package/errors.js +1 -0
- package/package.json +3 -3
package/auth.js
CHANGED
|
@@ -5,6 +5,8 @@ import { join } from 'node:path';
|
|
|
5
5
|
import open from 'open';
|
|
6
6
|
import { log } from './log.js';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { sendError } from './analytics.js';
|
|
9
|
+
import { matchErrorCode } from './errors.js';
|
|
8
10
|
// oauth server timeouts
|
|
9
11
|
const SERVER_TIMEOUT = 10000;
|
|
10
12
|
// where to wait for incoming redirect request from oauth server to arrive
|
|
@@ -18,6 +20,7 @@ const NEONCTL_SCOPES = [
|
|
|
18
20
|
'urn:neoncloud:projects:update',
|
|
19
21
|
'urn:neoncloud:projects:delete',
|
|
20
22
|
];
|
|
23
|
+
const AUTH_TIMEOUT_SECONDS = 60;
|
|
21
24
|
export const defaultClientID = 'neonctl';
|
|
22
25
|
custom.setHttpOptionsDefaults({
|
|
23
26
|
timeout: SERVER_TIMEOUT,
|
|
@@ -38,6 +41,7 @@ export const auth = async ({ oauthHost, clientId }) => {
|
|
|
38
41
|
//
|
|
39
42
|
// Start HTTP server and wait till /callback is hit
|
|
40
43
|
//
|
|
44
|
+
log.debug('Starting HTTP Server for callback');
|
|
41
45
|
const server = createServer();
|
|
42
46
|
server.listen(0, '127.0.0.1', function () {
|
|
43
47
|
log.debug(`Listening on port ${this.address().port}`);
|
|
@@ -55,7 +59,10 @@ export const auth = async ({ oauthHost, clientId }) => {
|
|
|
55
59
|
// we store the code_verifier in memory
|
|
56
60
|
const codeVerifier = generators.codeVerifier();
|
|
57
61
|
const codeChallenge = generators.codeChallenge(codeVerifier);
|
|
58
|
-
return new Promise((resolve) => {
|
|
62
|
+
return new Promise((resolve, reject) => {
|
|
63
|
+
const timer = setTimeout(() => {
|
|
64
|
+
reject(new Error(`Authentication timed out after ${AUTH_TIMEOUT_SECONDS} seconds`));
|
|
65
|
+
}, AUTH_TIMEOUT_SECONDS * 1000);
|
|
59
66
|
server.on('request', async (request, response) => {
|
|
60
67
|
//
|
|
61
68
|
// Wait for callback and follow oauth flow.
|
|
@@ -83,6 +90,7 @@ export const auth = async ({ oauthHost, clientId }) => {
|
|
|
83
90
|
});
|
|
84
91
|
response.writeHead(200, { 'Content-Type': 'text/html' });
|
|
85
92
|
createReadStream(join(fileURLToPath(new URL('.', import.meta.url)), './callback.html')).pipe(response);
|
|
93
|
+
clearTimeout(timer);
|
|
86
94
|
resolve(tokenSet);
|
|
87
95
|
server.close();
|
|
88
96
|
});
|
|
@@ -96,6 +104,12 @@ export const auth = async ({ oauthHost, clientId }) => {
|
|
|
96
104
|
code_challenge: codeChallenge,
|
|
97
105
|
code_challenge_method: 'S256',
|
|
98
106
|
});
|
|
99
|
-
|
|
107
|
+
log.info('Awaiting authentication in web browser.');
|
|
108
|
+
log.info(`Auth Url: ${authUrl}`);
|
|
109
|
+
open(authUrl).catch((err) => {
|
|
110
|
+
const msg = `Failed to open web browser. Please copy & paste auth url to authenticate in browser.`;
|
|
111
|
+
sendError(err || new Error(msg), matchErrorCode(msg || err?.message));
|
|
112
|
+
log.error(msg || err?.message);
|
|
113
|
+
});
|
|
100
114
|
});
|
|
101
115
|
};
|
package/commands/auth.test.js
CHANGED
|
@@ -95,20 +95,22 @@ export const handler = async (props) => {
|
|
|
95
95
|
.map((r) => r.name)
|
|
96
96
|
.join(', ')}`);
|
|
97
97
|
}));
|
|
98
|
+
const { data: { databases: branchDatabases }, } = await props.apiClient.listProjectBranchDatabases(projectId, branchId);
|
|
98
99
|
const database = props.databaseName ||
|
|
99
|
-
(
|
|
100
|
-
.
|
|
101
|
-
.then(({ data }) => {
|
|
102
|
-
if (data.databases.length === 0) {
|
|
100
|
+
(() => {
|
|
101
|
+
if (branchDatabases.length === 0) {
|
|
103
102
|
throw new Error(`No databases found for the branch: ${branchId}`);
|
|
104
103
|
}
|
|
105
|
-
if (
|
|
106
|
-
return
|
|
104
|
+
if (branchDatabases.length === 1) {
|
|
105
|
+
return branchDatabases[0].name;
|
|
107
106
|
}
|
|
108
|
-
throw new Error(`Multiple databases found for the branch, please provide one with the --database-name option: ${
|
|
107
|
+
throw new Error(`Multiple databases found for the branch, please provide one with the --database-name option: ${branchDatabases
|
|
109
108
|
.map((d) => d.name)
|
|
110
109
|
.join(', ')}`);
|
|
111
|
-
}));
|
|
110
|
+
})();
|
|
111
|
+
if (!branchDatabases.find((d) => d.name === database)) {
|
|
112
|
+
throw new Error(`Database not found: ${database}`);
|
|
113
|
+
}
|
|
112
114
|
const { data: { password }, } = await props.apiClient.getProjectBranchRolePassword(props.projectId, endpoint.branch_id, role);
|
|
113
115
|
let host = props.pooled
|
|
114
116
|
? endpoint.host.replace(endpoint.id, `${endpoint.id}-pooler`)
|
|
@@ -116,7 +118,7 @@ export const handler = async (props) => {
|
|
|
116
118
|
if (parsedPIT.tag !== 'head') {
|
|
117
119
|
host = endpoint.host.replace(endpoint.id, endpoint.branch_id);
|
|
118
120
|
}
|
|
119
|
-
const connectionString = new URL(`
|
|
121
|
+
const connectionString = new URL(`postgresql://${host}`);
|
|
120
122
|
connectionString.pathname = database;
|
|
121
123
|
connectionString.username = role;
|
|
122
124
|
connectionString.password = password;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe } from '@jest/globals';
|
|
1
|
+
import { describe, expect } from '@jest/globals';
|
|
2
2
|
import { testCliCommand } from '../test_utils/test_cli_command';
|
|
3
3
|
describe('connection_string', () => {
|
|
4
4
|
testCliCommand({
|
|
@@ -233,4 +233,21 @@ describe('connection_string', () => {
|
|
|
233
233
|
snapshot: true,
|
|
234
234
|
},
|
|
235
235
|
});
|
|
236
|
+
testCliCommand({
|
|
237
|
+
name: 'connection_string fails for non-existing database',
|
|
238
|
+
args: [
|
|
239
|
+
'connection-string',
|
|
240
|
+
'test_branch',
|
|
241
|
+
'--project-id',
|
|
242
|
+
'test',
|
|
243
|
+
'--database-name',
|
|
244
|
+
'non_existing_db',
|
|
245
|
+
'--role-name',
|
|
246
|
+
'test_role',
|
|
247
|
+
],
|
|
248
|
+
expected: {
|
|
249
|
+
code: 1,
|
|
250
|
+
stderr: expect.stringMatching(/Database not found/),
|
|
251
|
+
},
|
|
252
|
+
});
|
|
236
253
|
});
|
package/errors.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const ERROR_MATCHERS = [
|
|
2
2
|
[/^Unknown command: (.*)$/, 'UNKNOWN_COMMAND'],
|
|
3
3
|
[/^Missing required argument: (.*)$/, 'MISSING_ARGUMENT'],
|
|
4
|
+
[/^Failed to open web browser. (.*)$/, 'AUTH_BROWSER_FAILED'],
|
|
4
5
|
];
|
|
5
6
|
export const matchErrorCode = (message) => {
|
|
6
7
|
if (!message) {
|
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"url": "git+ssh://git@github.com/neondatabase/neonctl.git"
|
|
6
6
|
},
|
|
7
7
|
"type": "module",
|
|
8
|
-
"version": "1.29.
|
|
8
|
+
"version": "1.29.5",
|
|
9
9
|
"description": "CLI tool for NeonDB Cloud management",
|
|
10
10
|
"main": "index.js",
|
|
11
11
|
"author": "NeonDB",
|
|
@@ -60,8 +60,8 @@
|
|
|
60
60
|
"chalk": "^5.2.0",
|
|
61
61
|
"cli-table": "^0.3.11",
|
|
62
62
|
"inquirer": "^9.2.6",
|
|
63
|
-
"open": "^
|
|
64
|
-
"openid-client": "^5.
|
|
63
|
+
"open": "^10.1.0",
|
|
64
|
+
"openid-client": "^5.6.5",
|
|
65
65
|
"which": "^3.0.1",
|
|
66
66
|
"yaml": "^2.1.1",
|
|
67
67
|
"yargs": "^17.7.2"
|