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 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
- open(authUrl);
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
  };
@@ -6,7 +6,7 @@ import { runMockServer } from '../test_utils/mock_server';
6
6
  jest.unstable_mockModule('open', () => ({
7
7
  __esModule: true,
8
8
  default: jest.fn((url) => {
9
- axios.get(url);
9
+ return axios.get(url);
10
10
  }),
11
11
  }));
12
12
  // "open" module should be imported after mocking
@@ -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
- (await props.apiClient
100
- .listProjectBranchDatabases(projectId, branchId)
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 (data.databases.length === 1) {
106
- return data.databases[0].name;
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: ${data.databases
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(`postgres://${host}`);
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.3",
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": "^8.4.0",
64
- "openid-client": "^5.1.9",
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"