neonctl 1.1.0 → 1.2.0

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/api.js ADDED
@@ -0,0 +1,3 @@
1
+ import { createApiClient } from '@neondatabase/api-client';
2
+ export const getApiClient = ({ apiKey, apiHost }) => createApiClient({ apiKey, baseURL: apiHost });
3
+ export const isApiError = (err) => err instanceof Error && 'response' in err;
package/auth.js ADDED
@@ -0,0 +1,90 @@
1
+ import { custom, generators, Issuer } from 'openid-client';
2
+ import { createServer } from 'node:http';
3
+ import { createReadStream } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import open from 'open';
6
+ import { log } from './log.js';
7
+ import { fileURLToPath } from 'node:url';
8
+ // oauth server timeouts
9
+ const SERVER_TIMEOUT = 10000;
10
+ // where to wait for incoming redirect request from oauth server to arrive
11
+ const REDIRECT_URI = (port) => `http://127.0.0.1:${port}/callback`;
12
+ // These scopes cannot be cancelled, they are always needed.
13
+ const ALWAYS_PRESENT_SCOPES = ['openid', 'offline', 'offline_access'];
14
+ const NEONCTL_SCOPES = [
15
+ ...ALWAYS_PRESENT_SCOPES,
16
+ 'urn:neoncloud:projects:create',
17
+ 'urn:neoncloud:projects:read',
18
+ 'urn:neoncloud:projects:update',
19
+ 'urn:neoncloud:projects:delete',
20
+ ];
21
+ export const defaultClientID = 'neonctl';
22
+ custom.setHttpOptionsDefaults({
23
+ timeout: SERVER_TIMEOUT,
24
+ });
25
+ export const refreshToken = async ({ oauthHost, clientId }, tokenSet) => {
26
+ log.info('Discovering oauth server');
27
+ const issuer = await Issuer.discover(oauthHost);
28
+ const neonOAuthClient = new issuer.Client({
29
+ token_endpoint_auth_method: 'none',
30
+ client_id: clientId,
31
+ response_types: ['code'],
32
+ });
33
+ return await neonOAuthClient.refresh(tokenSet);
34
+ };
35
+ export const auth = async ({ oauthHost, clientId }) => {
36
+ log.info('Discovering oauth server');
37
+ const issuer = await Issuer.discover(oauthHost);
38
+ //
39
+ // Start HTTP server and wait till /callback is hit
40
+ //
41
+ const server = createServer();
42
+ server.listen(0, function () {
43
+ log.info(`Listening on port ${this.address().port}`);
44
+ });
45
+ const listen_port = server.address().port;
46
+ const neonOAuthClient = new issuer.Client({
47
+ token_endpoint_auth_method: 'none',
48
+ client_id: clientId,
49
+ redirect_uris: [REDIRECT_URI(listen_port)],
50
+ response_types: ['code'],
51
+ });
52
+ // https://datatracker.ietf.org/doc/html/rfc6819#section-4.4.1.8
53
+ const state = generators.state();
54
+ // we store the code_verifier in memory
55
+ const codeVerifier = generators.codeVerifier();
56
+ const codeChallenge = generators.codeChallenge(codeVerifier);
57
+ return new Promise((resolve) => {
58
+ server.on('request', async (request, response) => {
59
+ //
60
+ // Wait for callback and follow oauth flow.
61
+ //
62
+ if (!request.url?.startsWith('/callback')) {
63
+ response.writeHead(404);
64
+ response.end();
65
+ return;
66
+ }
67
+ log.info(`Callback received: ${request.url}`);
68
+ const params = neonOAuthClient.callbackParams(request);
69
+ const tokenSet = await neonOAuthClient.callback(REDIRECT_URI(listen_port), params, {
70
+ code_verifier: codeVerifier,
71
+ state,
72
+ });
73
+ response.writeHead(200, { 'Content-Type': 'text/html' });
74
+ createReadStream(join(fileURLToPath(new URL('.', import.meta.url)), './callback.html')).pipe(response);
75
+ resolve(tokenSet);
76
+ server.close();
77
+ });
78
+ //
79
+ // Open browser to let user authenticate
80
+ //
81
+ const scopes = clientId == defaultClientID ? NEONCTL_SCOPES : ALWAYS_PRESENT_SCOPES;
82
+ const authUrl = neonOAuthClient.authorizationUrl({
83
+ scope: scopes.join(' '),
84
+ state,
85
+ code_challenge: codeChallenge,
86
+ code_challenge_method: 'S256',
87
+ });
88
+ open(authUrl);
89
+ });
90
+ };
package/cli.js ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import './index.js';
@@ -0,0 +1,87 @@
1
+ import { join } from 'node:path';
2
+ import { writeFileSync, existsSync, readFileSync } from 'node:fs';
3
+ import { TokenSet } from 'openid-client';
4
+ import { auth, refreshToken } from '../auth.js';
5
+ import { log } from '../log.js';
6
+ import { getApiClient } from '../api.js';
7
+ const CREDENTIALS_FILE = 'credentials.json';
8
+ export const command = 'auth';
9
+ export const describe = 'Authenticate';
10
+ export const builder = (yargs) => yargs;
11
+ export const handler = async (args) => {
12
+ await authFlow(args);
13
+ };
14
+ export const authFlow = async ({ configDir, oauthHost, clientId, }) => {
15
+ if (!clientId) {
16
+ throw new Error('Missing client id');
17
+ }
18
+ const tokenSet = await auth({
19
+ oauthHost: oauthHost,
20
+ clientId: clientId,
21
+ });
22
+ const credentialsPath = join(configDir, CREDENTIALS_FILE);
23
+ updateCredentialsFile(credentialsPath, JSON.stringify(tokenSet));
24
+ log.info(`Saved credentials to ${credentialsPath}`);
25
+ log.info('Auth complete');
26
+ return tokenSet.access_token || '';
27
+ };
28
+ // updateCredentialsFile correctly sets needed permissions for the credentials file
29
+ function updateCredentialsFile(path, contents) {
30
+ writeFileSync(path, contents, {
31
+ mode: 0o700,
32
+ });
33
+ }
34
+ export const ensureAuth = async (props) => {
35
+ if (props._.length === 0) {
36
+ return;
37
+ }
38
+ if (props.apiKey || props._[0] === 'auth') {
39
+ props.apiClient = getApiClient({
40
+ apiKey: props.apiKey,
41
+ apiHost: props.apiHost,
42
+ });
43
+ return;
44
+ }
45
+ const credentialsPath = join(props.configDir, CREDENTIALS_FILE);
46
+ if (existsSync(credentialsPath)) {
47
+ try {
48
+ const tokenSetContents = await JSON.parse(readFileSync(credentialsPath, 'utf8'));
49
+ const tokenSet = new TokenSet(tokenSetContents);
50
+ if (tokenSet.expired()) {
51
+ log.info('using refresh token to update access token');
52
+ const refreshedTokenSet = await refreshToken({
53
+ oauthHost: props.oauthHost,
54
+ clientId: props.clientId,
55
+ }, tokenSet);
56
+ props.apiKey = refreshedTokenSet.access_token || 'UNKNOWN';
57
+ props.apiClient = getApiClient({
58
+ apiKey: props.apiKey,
59
+ apiHost: props.apiHost,
60
+ });
61
+ updateCredentialsFile(credentialsPath, JSON.stringify(refreshedTokenSet));
62
+ return;
63
+ }
64
+ const token = tokenSet.access_token || 'UNKNOWN';
65
+ props.apiKey = token;
66
+ props.apiClient = getApiClient({
67
+ apiKey: props.apiKey,
68
+ apiHost: props.apiHost,
69
+ });
70
+ return;
71
+ }
72
+ catch (e) {
73
+ if (e.code !== 'ENOENT') {
74
+ // not a "file does not exist" error
75
+ throw e;
76
+ }
77
+ props.apiKey = await authFlow(props);
78
+ }
79
+ }
80
+ else {
81
+ props.apiKey = await authFlow(props);
82
+ }
83
+ props.apiClient = getApiClient({
84
+ apiKey: props.apiKey,
85
+ apiHost: props.apiHost,
86
+ });
87
+ };
@@ -0,0 +1,4 @@
1
+ import * as auth from './auth.js';
2
+ import * as projects from './projects.js';
3
+ import * as users from './users.js';
4
+ export default [auth, projects, users];
@@ -0,0 +1,68 @@
1
+ import { projectCreateRequest } from '../parameters.gen.js';
2
+ import { writeOut } from '../writer.js';
3
+ const PROJECT_FIELDS = ['id', 'name', 'region_id', 'created_at'];
4
+ export const command = 'projects [command]';
5
+ export const describe = 'Manage projects';
6
+ export const builder = (yargs) => {
7
+ return yargs
8
+ .demandCommand(1, '')
9
+ .fail((msg, err, yargs) => {
10
+ yargs.showHelp();
11
+ process.exit(1);
12
+ })
13
+ .usage('usage: $0 projects <cmd> [args]')
14
+ .command('list', 'List projects', (yargs) => yargs, async (args) => {
15
+ await list(args);
16
+ })
17
+ .command('create', 'Create a project', (yargs) => yargs.options(projectCreateRequest), async (args) => {
18
+ await create(args);
19
+ })
20
+ .command('update', 'Update a project', (yargs) => yargs
21
+ .option('project.id', {
22
+ describe: 'Project ID',
23
+ type: 'string',
24
+ demandOption: true,
25
+ })
26
+ .options(projectCreateRequest), async (args) => {
27
+ await update(args);
28
+ })
29
+ .command('delete', 'Delete a project', (yargs) => yargs.options({
30
+ 'project.id': {
31
+ describe: 'Project ID',
32
+ type: 'string',
33
+ demandOption: true,
34
+ },
35
+ }), async (args) => {
36
+ await deleteProject(args);
37
+ });
38
+ };
39
+ export const handler = (args) => {
40
+ return args;
41
+ };
42
+ const list = async (props) => {
43
+ writeOut(props)((await props.apiClient.listProjects({})).data.projects, {
44
+ fields: PROJECT_FIELDS,
45
+ });
46
+ };
47
+ const create = async (props) => {
48
+ if (props.project == null) {
49
+ const inquirer = await import('inquirer');
50
+ const answers = await inquirer.default.prompt([
51
+ { name: 'name', message: 'Project name', type: 'input' },
52
+ ]);
53
+ props.project = answers;
54
+ }
55
+ writeOut(props)((await props.apiClient.createProject({
56
+ project: props.project,
57
+ })).data.project, { fields: PROJECT_FIELDS });
58
+ };
59
+ const deleteProject = async (props) => {
60
+ writeOut(props)((await props.apiClient.deleteProject(props.project.id)).data.project, {
61
+ fields: PROJECT_FIELDS,
62
+ });
63
+ };
64
+ const update = async (props) => {
65
+ writeOut(props)((await props.apiClient.updateProject(props.project.id, {
66
+ project: props.project,
67
+ })).data.project, { fields: PROJECT_FIELDS });
68
+ };
@@ -0,0 +1,12 @@
1
+ import { writeOut } from '../writer.js';
2
+ export const command = 'me';
3
+ export const describe = 'Show current user';
4
+ export const builder = (yargs) => yargs;
5
+ export const handler = async (args) => {
6
+ await me(args);
7
+ };
8
+ const me = async (props) => {
9
+ writeOut(props)((await props.apiClient.getCurrentUserInfo()).data, {
10
+ fields: ['login', 'email', 'name', 'projects_limit'],
11
+ });
12
+ };
package/config.js ADDED
@@ -0,0 +1,9 @@
1
+ import { join } from 'node:path';
2
+ import { homedir } from 'node:os';
3
+ import { existsSync, mkdirSync } from 'node:fs';
4
+ export const defaultDir = join(homedir(), process.env.XDG_CONFIG_HOME || '.config', 'neonctl');
5
+ export const ensureConfigDir = async ({ 'config-dir': configDir, }) => {
6
+ if (!existsSync(configDir)) {
7
+ mkdirSync(configDir);
8
+ }
9
+ };
package/index.js ADDED
@@ -0,0 +1,78 @@
1
+ import yargs from 'yargs';
2
+ import { hideBin } from 'yargs/helpers';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { readFileSync } from 'node:fs';
5
+ import { ensureAuth } from './commands/auth.js';
6
+ import { defaultDir, ensureConfigDir } from './config.js';
7
+ import { log } from './log.js';
8
+ import { defaultClientID } from './auth.js';
9
+ import { isApiError } from './api.js';
10
+ import { fillInArgs } from './utils.js';
11
+ import commands from './commands/index.js';
12
+ const pkg = JSON.parse(readFileSync(fileURLToPath(new URL('./package.json', import.meta.url)), 'utf-8'));
13
+ const builder = yargs(hideBin(process.argv))
14
+ .scriptName(pkg.name)
15
+ .usage('usage: $0 <cmd> [args]')
16
+ .help()
17
+ .option('output', {
18
+ describe: 'Set output format',
19
+ type: 'string',
20
+ choices: ['json', 'yaml', 'table'],
21
+ default: 'table',
22
+ })
23
+ .option('api-host', {
24
+ describe: 'The API host',
25
+ default: 'https://console.neon.tech/api/v2',
26
+ })
27
+ // Setup config directory
28
+ .option('config-dir', {
29
+ describe: 'Path to config directory',
30
+ type: 'string',
31
+ default: defaultDir,
32
+ })
33
+ .middleware(ensureConfigDir)
34
+ // Auth flow
35
+ .option('oauth-host', {
36
+ description: 'URL to Neon OAUTH host',
37
+ default: 'https://oauth2.neon.tech',
38
+ })
39
+ .option('client-id', {
40
+ description: 'OAuth client id',
41
+ type: 'string',
42
+ default: defaultClientID,
43
+ })
44
+ .option('api-key', {
45
+ describe: 'API key',
46
+ type: 'string',
47
+ default: '',
48
+ })
49
+ .option('apiClient', {
50
+ hidden: true,
51
+ coerce: (v) => v,
52
+ default: true,
53
+ })
54
+ .middleware((args) => fillInArgs(args), true)
55
+ .middleware(ensureAuth)
56
+ .command(commands)
57
+ .strictCommands()
58
+ .fail(async (msg, err) => {
59
+ if (isApiError(err)) {
60
+ if (err.response.status === 401) {
61
+ log.error('Authentication failed, please run `neonctl auth`');
62
+ }
63
+ else {
64
+ log.error('%d: %s\n%s', err.response.status, err.response.statusText, err.response.data?.message);
65
+ }
66
+ }
67
+ else {
68
+ log.error(msg || err?.message);
69
+ }
70
+ process.exit(1);
71
+ });
72
+ (async () => {
73
+ const args = await builder.argv;
74
+ if (args._.length === 0) {
75
+ builder.showHelp();
76
+ process.exit(0);
77
+ }
78
+ })();
package/log.js ADDED
@@ -0,0 +1,9 @@
1
+ import { format } from 'node:util';
2
+ export const log = {
3
+ info: (...args) => {
4
+ process.stderr.write(`INFO: ${format(...args)}\n`);
5
+ },
6
+ error: (...args) => {
7
+ process.stderr.write(`ERROR: ${format(...args)}\n`);
8
+ },
9
+ };
package/package.json CHANGED
@@ -1,63 +1,65 @@
1
1
  {
2
- "name": "neonctl",
3
- "repository": {
4
- "type": "git",
5
- "url": "git@github.com:neondatabase/neonctl.git"
6
- },
7
- "version": "1.1.0",
8
- "description": "CLI tool for NeonDB Cloud management",
9
- "main": "index.js",
10
- "author": "NeonDB",
11
- "license": "MIT",
12
- "private": false,
13
- "engines": {
14
- "node": ">=18"
15
- },
16
- "bin": {
17
- "neonctl": "src/cli.js"
18
- },
19
- "devDependencies": {
20
- "@semantic-release/git": "^10.0.1",
21
- "@types/cli-table": "^0.3.0",
22
- "@types/node": "^18.7.13",
23
- "@types/yargs": "^17.0.24",
24
- "@typescript-eslint/eslint-plugin": "^5.34.0",
25
- "@typescript-eslint/parser": "^5.34.0",
26
- "eslint": "^8.22.0",
27
- "husky": "^8.0.1",
28
- "lint-staged": "^13.0.3",
29
- "prettier": "^2.7.1",
30
- "semantic-release": "^21.0.2",
31
- "ts-morph": "^18.0.0",
32
- "ts-node": "^10.9.1",
33
- "typescript": "^4.7.4"
34
- },
35
- "dependencies": {
36
- "@neondatabase/api-client": "^0.1.0",
37
- "axios": "^1.4.0",
38
- "cli-table": "^0.3.11",
39
- "open": "^8.4.0",
40
- "openid-client": "^5.1.9",
41
- "yaml": "^2.1.1",
42
- "yargs": "^17.7.2"
43
- },
44
- "publishConfig": {
45
- "access": "public",
46
- "registry": "https://registry.npmjs.org/"
47
- },
48
- "scripts": {
49
- "dev": "node dev.js",
50
- "lint": "tsc --noEmit && eslint src --ext .ts",
51
- "debug": "node --inspect-brk dev.js",
52
- "build": "npm run clean && tsc && cp src/*.html dist/src/",
53
- "clean": "rm -rf dist",
54
- "start": "node index.js",
55
- "postinstall": "ts-node ./generateParams.ts"
56
- },
57
- "lint-staged": {
58
- "*.ts": [
59
- "eslint --cache --fix",
60
- "prettier --write"
61
- ]
62
- }
2
+ "name": "neonctl",
3
+ "repository": {
4
+ "type": "git",
5
+ "url": "git@github.com:neondatabase/neonctl.git"
6
+ },
7
+ "type": "module",
8
+ "version": "1.2.0",
9
+ "description": "CLI tool for NeonDB Cloud management",
10
+ "main": "index.js",
11
+ "author": "NeonDB",
12
+ "license": "MIT",
13
+ "private": false,
14
+ "engines": {
15
+ "node": ">=18"
16
+ },
17
+ "bin": {
18
+ "neonctl": "cli.js"
19
+ },
20
+ "devDependencies": {
21
+ "@semantic-release/git": "^10.0.1",
22
+ "@types/cli-table": "^0.3.0",
23
+ "@types/inquirer": "^9.0.3",
24
+ "@types/node": "^18.7.13",
25
+ "@types/yargs": "^17.0.24",
26
+ "@typescript-eslint/eslint-plugin": "^5.34.0",
27
+ "@typescript-eslint/parser": "^5.34.0",
28
+ "eslint": "^8.22.0",
29
+ "husky": "^8.0.1",
30
+ "lint-staged": "^13.0.3",
31
+ "prettier": "^2.7.1",
32
+ "semantic-release": "^21.0.2",
33
+ "ts-morph": "^18.0.0",
34
+ "ts-node": "^10.9.1",
35
+ "typescript": "^4.7.4"
36
+ },
37
+ "dependencies": {
38
+ "@neondatabase/api-client": "^0.1.0",
39
+ "axios": "^1.4.0",
40
+ "cli-table": "^0.3.11",
41
+ "inquirer": "^9.2.6",
42
+ "open": "^8.4.0",
43
+ "openid-client": "^5.1.9",
44
+ "yaml": "^2.1.1",
45
+ "yargs": "^17.7.2"
46
+ },
47
+ "publishConfig": {
48
+ "access": "public",
49
+ "registry": "https://registry.npmjs.org/"
50
+ },
51
+ "scripts": {
52
+ "watch": "tsc --watch",
53
+ "lint": "tsc --noEmit && eslint src --ext .ts",
54
+ "build": "npm run generateParams && npm run clean && tsc && cp src/*.html dist/ && cp package.json ./dist",
55
+ "clean": "rm -rf dist",
56
+ "generateParams": "ts-node --esm generateParams.ts",
57
+ "start": "node src/index.js"
58
+ },
59
+ "lint-staged": {
60
+ "*.ts": [
61
+ "eslint --cache --fix",
62
+ "prettier --write"
63
+ ]
64
+ }
63
65
  }
@@ -1,8 +1,5 @@
1
- "use strict";
2
1
  // FILE IS GENERATED, DO NOT EDIT
3
- Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.ProjectCreateRequest = void 0;
5
- exports.ProjectCreateRequest = {
2
+ export const projectCreateRequest = {
6
3
  'project.settings.quota.active_time_seconds': {
7
4
  type: 'number',
8
5
  description: "The total amount of wall-clock time allowed to be spent by project's compute endpoints.",
package/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/utils.js ADDED
@@ -0,0 +1,20 @@
1
+ /**
2
+ * This middleware is needed to fill in the args for nested objects,
3
+ * so that required arguments would work
4
+ * otherwise yargs just throws an error
5
+ */
6
+ export const fillInArgs = (args, currentArgs = args, acc = []) => {
7
+ Object.entries(currentArgs).forEach(([k, v]) => {
8
+ if (k === '_') {
9
+ return;
10
+ }
11
+ // check if the value is an Object
12
+ if (typeof v === 'object' && v !== null) {
13
+ fillInArgs(args, v, [...acc, k]);
14
+ }
15
+ else if (acc.length > 0) {
16
+ // if it's not an object, and we have a path, fill it in
17
+ args[acc.join('.') + '.' + k] = v;
18
+ }
19
+ });
20
+ };
@@ -1,20 +1,14 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.writeOut = void 0;
7
- const yaml_1 = __importDefault(require("yaml"));
8
- const cli_table_1 = __importDefault(require("cli-table"));
1
+ import YAML from 'yaml';
2
+ import Table from 'cli-table';
9
3
  // Allow PIPE to finish reading before the end of the output.
10
4
  process.stdout.on('error', function (err) {
11
5
  if (err.code == 'EPIPE') {
12
6
  process.exit(0);
13
7
  }
14
8
  });
15
- const writeOut = (props) => (data, config) => {
9
+ export const writeOut = (props) => (data, config) => {
16
10
  if (props.output == 'yaml') {
17
- process.stdout.write(yaml_1.default.stringify(data, null, 2));
11
+ process.stdout.write(YAML.stringify(data, null, 2));
18
12
  return;
19
13
  }
20
14
  if (props.output == 'json') {
@@ -22,7 +16,7 @@ const writeOut = (props) => (data, config) => {
22
16
  return;
23
17
  }
24
18
  const arrayData = Array.isArray(data) ? data : [data];
25
- const table = new cli_table_1.default({
19
+ const table = new Table({
26
20
  style: {
27
21
  head: ['green'],
28
22
  },
@@ -32,9 +26,8 @@ const writeOut = (props) => (data, config) => {
32
26
  .join(' ')),
33
27
  });
34
28
  arrayData.forEach((item) => {
35
- table.push(config.fields.map((field) => { var _a; return (_a = item[field]) !== null && _a !== void 0 ? _a : ''; }));
29
+ table.push(config.fields.map((field) => item[field] ?? ''));
36
30
  });
37
31
  process.stdout.write(table.toString());
38
32
  process.stdout.write('\n');
39
33
  };
40
- exports.writeOut = writeOut;
package/src/api.js DELETED
@@ -1,8 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.isApiError = exports.getApiClient = void 0;
4
- const api_client_1 = require("@neondatabase/api-client");
5
- const getApiClient = ({ apiKey, apiHost }) => (0, api_client_1.createApiClient)({ apiKey, baseURL: apiHost });
6
- exports.getApiClient = getApiClient;
7
- const isApiError = (err) => err instanceof Error && 'response' in err;
8
- exports.isApiError = isApiError;
package/src/auth.js DELETED
@@ -1,107 +0,0 @@
1
- "use strict";
2
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
- return new (P || (P = Promise))(function (resolve, reject) {
5
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
- step((generator = generator.apply(thisArg, _arguments || [])).next());
9
- });
10
- };
11
- var __importDefault = (this && this.__importDefault) || function (mod) {
12
- return (mod && mod.__esModule) ? mod : { "default": mod };
13
- };
14
- Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.auth = exports.refreshToken = exports.defaultClientID = void 0;
16
- const openid_client_1 = require("openid-client");
17
- const node_http_1 = require("node:http");
18
- const node_fs_1 = require("node:fs");
19
- const node_path_1 = require("node:path");
20
- const open_1 = __importDefault(require("open"));
21
- const log_1 = require("./log");
22
- // oauth server timeouts
23
- const SERVER_TIMEOUT = 10000;
24
- // where to wait for incoming redirect request from oauth server to arrive
25
- const REDIRECT_URI = (port) => `http://127.0.0.1:${port}/callback`;
26
- // These scopes cannot be cancelled, they are always needed.
27
- const ALWAYS_PRESENT_SCOPES = ['openid', 'offline', 'offline_access'];
28
- const NEONCTL_SCOPES = [
29
- ...ALWAYS_PRESENT_SCOPES,
30
- 'urn:neoncloud:projects:create',
31
- 'urn:neoncloud:projects:read',
32
- 'urn:neoncloud:projects:update',
33
- 'urn:neoncloud:projects:delete',
34
- ];
35
- exports.defaultClientID = 'neonctl';
36
- openid_client_1.custom.setHttpOptionsDefaults({
37
- timeout: SERVER_TIMEOUT,
38
- });
39
- const refreshToken = ({ oauthHost, clientId }, tokenSet) => __awaiter(void 0, void 0, void 0, function* () {
40
- log_1.log.info('Discovering oauth server');
41
- const issuer = yield openid_client_1.Issuer.discover(oauthHost);
42
- const neonOAuthClient = new issuer.Client({
43
- token_endpoint_auth_method: 'none',
44
- client_id: clientId,
45
- response_types: ['code'],
46
- });
47
- return yield neonOAuthClient.refresh(tokenSet);
48
- });
49
- exports.refreshToken = refreshToken;
50
- const auth = ({ oauthHost, clientId }) => __awaiter(void 0, void 0, void 0, function* () {
51
- log_1.log.info('Discovering oauth server');
52
- const issuer = yield openid_client_1.Issuer.discover(oauthHost);
53
- //
54
- // Start HTTP server and wait till /callback is hit
55
- //
56
- const server = (0, node_http_1.createServer)();
57
- server.listen(0, function () {
58
- log_1.log.info(`Listening on port ${this.address().port}`);
59
- });
60
- const listen_port = server.address().port;
61
- const neonOAuthClient = new issuer.Client({
62
- token_endpoint_auth_method: 'none',
63
- client_id: clientId,
64
- redirect_uris: [REDIRECT_URI(listen_port)],
65
- response_types: ['code'],
66
- });
67
- // https://datatracker.ietf.org/doc/html/rfc6819#section-4.4.1.8
68
- const state = openid_client_1.generators.state();
69
- // we store the code_verifier in memory
70
- const codeVerifier = openid_client_1.generators.codeVerifier();
71
- const codeChallenge = openid_client_1.generators.codeChallenge(codeVerifier);
72
- return new Promise((resolve) => {
73
- server.on('request', (request, response) => __awaiter(void 0, void 0, void 0, function* () {
74
- var _a;
75
- //
76
- // Wait for callback and follow oauth flow.
77
- //
78
- if (!((_a = request.url) === null || _a === void 0 ? void 0 : _a.startsWith('/callback'))) {
79
- response.writeHead(404);
80
- response.end();
81
- return;
82
- }
83
- log_1.log.info(`Callback received: ${request.url}`);
84
- const params = neonOAuthClient.callbackParams(request);
85
- const tokenSet = yield neonOAuthClient.callback(REDIRECT_URI(listen_port), params, {
86
- code_verifier: codeVerifier,
87
- state,
88
- });
89
- response.writeHead(200, { 'Content-Type': 'text/html' });
90
- (0, node_fs_1.createReadStream)((0, node_path_1.join)(__dirname, './callback.html')).pipe(response);
91
- resolve(tokenSet);
92
- server.close();
93
- }));
94
- //
95
- // Open browser to let user authenticate
96
- //
97
- const scopes = clientId == exports.defaultClientID ? NEONCTL_SCOPES : ALWAYS_PRESENT_SCOPES;
98
- const authUrl = neonOAuthClient.authorizationUrl({
99
- scope: scopes.join(' '),
100
- state,
101
- code_challenge: codeChallenge,
102
- code_challenge_method: 'S256',
103
- });
104
- (0, open_1.default)(authUrl);
105
- });
106
- });
107
- exports.auth = auth;
package/src/cli.js DELETED
@@ -1,4 +0,0 @@
1
- #!/usr/bin/env node
2
- "use strict";
3
- Object.defineProperty(exports, "__esModule", { value: true });
4
- require("./index");
@@ -1,132 +0,0 @@
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 (mod) {
19
- if (mod && mod.__esModule) return mod;
20
- var result = {};
21
- if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
- __setModuleDefault(result, mod);
23
- return result;
24
- };
25
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
26
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
27
- return new (P || (P = Promise))(function (resolve, reject) {
28
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
29
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
30
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
31
- step((generator = generator.apply(thisArg, _arguments || [])).next());
32
- });
33
- };
34
- Object.defineProperty(exports, "__esModule", { value: true });
35
- exports.ensureAuth = exports.authFlow = void 0;
36
- const node_path_1 = require("node:path");
37
- const node_fs_1 = require("node:fs");
38
- const openid_client_1 = require("openid-client");
39
- const auth_1 = require("../auth");
40
- const log_1 = require("../log");
41
- const api_1 = require("../api");
42
- const CREDENTIALS_FILE = 'credentials.json';
43
- const authFlow = ({ configDir, oauthHost, clientId, }) => __awaiter(void 0, void 0, void 0, function* () {
44
- if (!clientId) {
45
- throw new Error('Missing client id');
46
- }
47
- const tokenSet = yield (0, auth_1.auth)({
48
- oauthHost: oauthHost,
49
- clientId: clientId,
50
- });
51
- const credentialsPath = (0, node_path_1.join)(configDir, CREDENTIALS_FILE);
52
- updateCredentialsFile(credentialsPath, JSON.stringify(tokenSet));
53
- log_1.log.info(`Saved credentials to ${credentialsPath}`);
54
- log_1.log.info('Auth complete');
55
- return tokenSet.access_token || '';
56
- });
57
- exports.authFlow = authFlow;
58
- const validateToken = (props) => __awaiter(void 0, void 0, void 0, function* () {
59
- try {
60
- const client = (0, api_1.getApiClient)(props);
61
- yield client.getCurrentUserInfo();
62
- }
63
- catch (err) {
64
- if ((0, api_1.isApiError)(err)) {
65
- if (err.response.status === 401) {
66
- throw new Error('Invalid token');
67
- }
68
- }
69
- }
70
- });
71
- // updateCredentialsFile correctly sets needed permissions for the credentials file
72
- function updateCredentialsFile(path, contents) {
73
- (0, node_fs_1.writeFileSync)(path, contents, {
74
- mode: 0o700,
75
- });
76
- }
77
- const ensureAuth = (props) => __awaiter(void 0, void 0, void 0, function* () {
78
- var _a;
79
- if (props._.length === 0) {
80
- return;
81
- }
82
- if (props.apiKey || props._[0] === 'auth') {
83
- props.apiClient = (0, api_1.getApiClient)({
84
- apiKey: props.apiKey,
85
- apiHost: props.apiHost,
86
- });
87
- return;
88
- }
89
- const credentialsPath = (0, node_path_1.join)(props.configDir, CREDENTIALS_FILE);
90
- if ((0, node_fs_1.existsSync)(credentialsPath)) {
91
- try {
92
- const tokenSetContents = yield (_a = credentialsPath, Promise.resolve().then(() => __importStar(require(_a))));
93
- const tokenSet = new openid_client_1.TokenSet(tokenSetContents);
94
- if (tokenSet.expired()) {
95
- log_1.log.info('using refresh token to update access token');
96
- const refreshedTokenSet = yield (0, auth_1.refreshToken)({
97
- oauthHost: props.oauthHost,
98
- clientId: props.clientId,
99
- }, tokenSet);
100
- props.apiKey = refreshedTokenSet.access_token || 'UNKNOWN';
101
- props.apiClient = (0, api_1.getApiClient)({
102
- apiKey: props.apiKey,
103
- apiHost: props.apiHost,
104
- });
105
- updateCredentialsFile(credentialsPath, JSON.stringify(refreshedTokenSet));
106
- return;
107
- }
108
- const token = tokenSet.access_token || 'UNKNOWN';
109
- props.apiKey = token;
110
- props.apiClient = (0, api_1.getApiClient)({
111
- apiKey: props.apiKey,
112
- apiHost: props.apiHost,
113
- });
114
- return;
115
- }
116
- catch (e) {
117
- if (e.code !== 'ENOENT') {
118
- // not a "file does not exist" error
119
- throw e;
120
- }
121
- props.apiKey = yield (0, exports.authFlow)(props);
122
- }
123
- }
124
- else {
125
- props.apiKey = yield (0, exports.authFlow)(props);
126
- }
127
- props.apiClient = (0, api_1.getApiClient)({
128
- apiKey: props.apiKey,
129
- apiHost: props.apiHost,
130
- });
131
- });
132
- exports.ensureAuth = ensureAuth;
@@ -1,38 +0,0 @@
1
- "use strict";
2
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
- return new (P || (P = Promise))(function (resolve, reject) {
5
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
- step((generator = generator.apply(thisArg, _arguments || [])).next());
9
- });
10
- };
11
- Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.update = exports.deleteProject = exports.create = exports.list = void 0;
13
- const writer_1 = require("../writer");
14
- const PROJECT_FIELDS = ['id', 'name', 'region_id', 'created_at'];
15
- const list = (props) => __awaiter(void 0, void 0, void 0, function* () {
16
- (0, writer_1.writeOut)(props)((yield props.apiClient.listProjects({})).data.projects, {
17
- fields: PROJECT_FIELDS,
18
- });
19
- });
20
- exports.list = list;
21
- const create = (props) => __awaiter(void 0, void 0, void 0, function* () {
22
- (0, writer_1.writeOut)(props)((yield props.apiClient.createProject({
23
- project: props.project,
24
- })).data.project, { fields: PROJECT_FIELDS });
25
- });
26
- exports.create = create;
27
- const deleteProject = (props) => __awaiter(void 0, void 0, void 0, function* () {
28
- (0, writer_1.writeOut)(props)((yield props.apiClient.deleteProject(props.project.id)).data.project, {
29
- fields: PROJECT_FIELDS,
30
- });
31
- });
32
- exports.deleteProject = deleteProject;
33
- const update = (props) => __awaiter(void 0, void 0, void 0, function* () {
34
- (0, writer_1.writeOut)(props)((yield props.apiClient.updateProject(props.project.id, {
35
- project: props.project,
36
- })).data.project, { fields: PROJECT_FIELDS });
37
- });
38
- exports.update = update;
@@ -1,19 +0,0 @@
1
- "use strict";
2
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
- return new (P || (P = Promise))(function (resolve, reject) {
5
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
- step((generator = generator.apply(thisArg, _arguments || [])).next());
9
- });
10
- };
11
- Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.me = void 0;
13
- const writer_1 = require("../writer");
14
- const me = (props) => __awaiter(void 0, void 0, void 0, function* () {
15
- (0, writer_1.writeOut)(props)((yield props.apiClient.getCurrentUserInfo()).data, {
16
- fields: ['login', 'email', 'name', 'projects_limit'],
17
- });
18
- });
19
- exports.me = me;
package/src/config.js DELETED
@@ -1,22 +0,0 @@
1
- "use strict";
2
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
- return new (P || (P = Promise))(function (resolve, reject) {
5
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
- step((generator = generator.apply(thisArg, _arguments || [])).next());
9
- });
10
- };
11
- Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.ensureConfigDir = exports.defaultDir = void 0;
13
- const node_path_1 = require("node:path");
14
- const node_os_1 = require("node:os");
15
- const node_fs_1 = require("node:fs");
16
- exports.defaultDir = (0, node_path_1.join)((0, node_os_1.homedir)(), process.env.XDG_CONFIG_HOME || '.config', 'neonctl');
17
- const ensureConfigDir = ({ 'config-dir': configDir, }) => __awaiter(void 0, void 0, void 0, function* () {
18
- if (!(0, node_fs_1.existsSync)(configDir)) {
19
- (0, node_fs_1.mkdirSync)(configDir);
20
- }
21
- });
22
- exports.ensureConfigDir = ensureConfigDir;
package/src/index.js DELETED
@@ -1,152 +0,0 @@
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 (mod) {
19
- if (mod && mod.__esModule) return mod;
20
- var result = {};
21
- if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
- __setModuleDefault(result, mod);
23
- return result;
24
- };
25
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
26
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
27
- return new (P || (P = Promise))(function (resolve, reject) {
28
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
29
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
30
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
31
- step((generator = generator.apply(thisArg, _arguments || [])).next());
32
- });
33
- };
34
- var __importDefault = (this && this.__importDefault) || function (mod) {
35
- return (mod && mod.__esModule) ? mod : { "default": mod };
36
- };
37
- Object.defineProperty(exports, "__esModule", { value: true });
38
- const yargs = __importStar(require("yargs"));
39
- const package_json_1 = __importDefault(require("../package.json"));
40
- const auth_1 = require("./commands/auth");
41
- const config_1 = require("./config");
42
- const log_1 = require("./log");
43
- const auth_2 = require("./auth");
44
- const api_1 = require("./api");
45
- const parameters_gen_1 = require("./parameters.gen");
46
- const utils_1 = require("./utils");
47
- const showHelpMiddleware = (argv) => {
48
- if (argv._.length === 1) {
49
- yargs.showHelp();
50
- process.exit(0);
51
- }
52
- };
53
- const builder = yargs
54
- .scriptName(package_json_1.default.name)
55
- .usage('usage: $0 <cmd> [args]')
56
- .help()
57
- .option('output', {
58
- describe: 'Set output format',
59
- type: 'string',
60
- choices: ['json', 'yaml', 'table'],
61
- default: 'table',
62
- })
63
- .option('api-host', {
64
- describe: 'The API host',
65
- default: 'https://console.neon.tech/api/v2',
66
- })
67
- // Setup config directory
68
- .option('config-dir', {
69
- describe: 'Path to config directory',
70
- type: 'string',
71
- default: config_1.defaultDir,
72
- })
73
- .middleware(config_1.ensureConfigDir)
74
- // Auth flow
75
- .option('oauth-host', {
76
- description: 'URL to Neon OAUTH host',
77
- default: 'https://oauth2.neon.tech',
78
- })
79
- .option('client-id', {
80
- description: 'OAuth client id',
81
- type: 'string',
82
- default: auth_2.defaultClientID,
83
- })
84
- .command('auth', 'Authenticate user', (yargs) => yargs, (args) => __awaiter(void 0, void 0, void 0, function* () {
85
- (yield Promise.resolve().then(() => __importStar(require('./commands/auth')))).authFlow(args);
86
- }))
87
- .option('api-key', {
88
- describe: 'API key',
89
- type: 'string',
90
- default: '',
91
- })
92
- .option('apiClient', {
93
- hidden: true,
94
- coerce: (v) => v,
95
- default: true,
96
- })
97
- .middleware((args) => (0, utils_1.fillInArgs)(args), true)
98
- .middleware(auth_1.ensureAuth)
99
- .command('me', 'Get user info', (yargs) => yargs, (args) => __awaiter(void 0, void 0, void 0, function* () {
100
- yield (yield Promise.resolve().then(() => __importStar(require('./commands/users')))).me(args);
101
- }))
102
- .command('projects', 'Manage projects', (yargs) => __awaiter(void 0, void 0, void 0, function* () {
103
- yargs
104
- .usage('usage: $0 projects <cmd> [args]')
105
- .command('list', 'List projects', (yargs) => yargs, (args) => __awaiter(void 0, void 0, void 0, function* () {
106
- yield (yield Promise.resolve().then(() => __importStar(require('./commands/projects')))).list(args);
107
- }))
108
- .command('create', 'Create a project', (yargs) => yargs.options(parameters_gen_1.ProjectCreateRequest), (args) => __awaiter(void 0, void 0, void 0, function* () {
109
- yield (yield Promise.resolve().then(() => __importStar(require('./commands/projects')))).create(args);
110
- }))
111
- .command('update', 'Update a project', (yargs) => yargs
112
- .option('project.id', {
113
- describe: 'Project ID',
114
- type: 'string',
115
- demandOption: true,
116
- })
117
- .options(parameters_gen_1.ProjectCreateRequest), (args) => __awaiter(void 0, void 0, void 0, function* () {
118
- yield (yield Promise.resolve().then(() => __importStar(require('./commands/projects')))).update(args);
119
- }))
120
- .command('delete', 'Delete a project', (yargs) => yargs.options({
121
- 'project.id': {
122
- describe: 'Project ID',
123
- type: 'string',
124
- demandOption: true,
125
- },
126
- }), (args) => __awaiter(void 0, void 0, void 0, function* () {
127
- yield (yield Promise.resolve().then(() => __importStar(require('./commands/projects')))).deleteProject(args);
128
- }))
129
- .middleware(showHelpMiddleware);
130
- }))
131
- .fail((msg, err) => __awaiter(void 0, void 0, void 0, function* () {
132
- var _a;
133
- if ((0, api_1.isApiError)(err)) {
134
- if (err.response.status === 401) {
135
- log_1.log.error('Authentication failed, please run `neonctl auth`');
136
- }
137
- else {
138
- log_1.log.error('%d: %s\n%s', err.response.status, err.response.statusText, (_a = err.response.data) === null || _a === void 0 ? void 0 : _a.message);
139
- }
140
- }
141
- else {
142
- log_1.log.error(msg || err.message);
143
- }
144
- process.exit(1);
145
- }));
146
- (() => __awaiter(void 0, void 0, void 0, function* () {
147
- const args = yield builder.argv;
148
- if (args._.length === 0) {
149
- yargs.showHelp();
150
- process.exit(0);
151
- }
152
- }))();
package/src/log.js DELETED
@@ -1,12 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.log = void 0;
4
- const node_util_1 = require("node:util");
5
- exports.log = {
6
- info: (...args) => {
7
- process.stderr.write(`INFO: ${(0, node_util_1.format)(...args)}\n`);
8
- },
9
- error: (...args) => {
10
- process.stderr.write(`ERROR: ${(0, node_util_1.format)(...args)}\n`);
11
- },
12
- };
package/src/types.js DELETED
@@ -1,2 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
package/src/utils.js DELETED
@@ -1,22 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.fillInArgs = void 0;
4
- // This middleware is needed to fill in the args for nested objects,
5
- // so that required arguments would work
6
- // otherwise yargs just throws an error
7
- const fillInArgs = (args, currentArgs = args, acc = []) => {
8
- Object.entries(currentArgs).forEach(([k, v]) => {
9
- if (k === '_') {
10
- return;
11
- }
12
- // check if the value is an Object
13
- if (typeof v === 'object' && v !== null) {
14
- (0, exports.fillInArgs)(args, v, [...acc, k]);
15
- }
16
- else if (acc.length > 0) {
17
- // if it's not an object, and we have a path, fill it in
18
- args[acc.join('.') + '.' + k] = v;
19
- }
20
- });
21
- };
22
- exports.fillInArgs = fillInArgs;
File without changes