neonctl 1.29.1 → 1.29.3

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.
Files changed (184) hide show
  1. package/analytics.js +78 -0
  2. package/api.js +35 -0
  3. package/auth.js +101 -0
  4. package/{src/cli.ts → cli.js} +0 -1
  5. package/commands/auth.js +102 -0
  6. package/commands/auth.test.js +42 -0
  7. package/commands/branches.js +303 -0
  8. package/commands/branches.test.js +321 -0
  9. package/commands/connection_string.js +156 -0
  10. package/commands/connection_string.test.js +236 -0
  11. package/commands/databases.js +79 -0
  12. package/commands/databases.test.js +51 -0
  13. package/commands/help.test.js +11 -0
  14. package/{src/commands/index.ts → commands/index.js} +10 -11
  15. package/commands/ip_allow.js +135 -0
  16. package/commands/ip_allow.test.js +78 -0
  17. package/commands/operations.js +28 -0
  18. package/commands/operations.test.js +11 -0
  19. package/commands/projects.js +186 -0
  20. package/commands/projects.test.js +132 -0
  21. package/commands/roles.js +57 -0
  22. package/commands/roles.test.js +42 -0
  23. package/commands/set_context.js +22 -0
  24. package/commands/set_context.test.js +53 -0
  25. package/commands/user.js +15 -0
  26. package/config.js +11 -0
  27. package/context.js +48 -0
  28. package/env.js +6 -0
  29. package/errors.js +16 -0
  30. package/help.js +146 -0
  31. package/index.js +168 -0
  32. package/log.js +15 -0
  33. package/package.json +1 -1
  34. package/parameters.gen.js +322 -0
  35. package/pkg.js +3 -45
  36. package/test_utils/mock_server.js +16 -0
  37. package/test_utils/oauth_server.js +9 -0
  38. package/test_utils/test_cli_command.js +80 -0
  39. package/types.js +1 -0
  40. package/utils/enrichers.js +49 -0
  41. package/utils/formats.js +5 -0
  42. package/utils/formats.test.js +32 -0
  43. package/utils/middlewares.js +20 -0
  44. package/utils/point_in_time.js +50 -0
  45. package/utils/psql.js +24 -0
  46. package/utils/string.js +5 -0
  47. package/utils/ui.js +59 -0
  48. package/writer.js +87 -0
  49. package/writer.test.js +86 -0
  50. package/.bump +0 -1
  51. package/.editorconfig +0 -7
  52. package/.eslintrc.cjs +0 -15
  53. package/.github/workflows/commitlint.yml +0 -46
  54. package/.github/workflows/pr.yml +0 -25
  55. package/.github/workflows/release.yml +0 -30
  56. package/.husky/commit-msg +0 -4
  57. package/.husky/pre-commit +0 -4
  58. package/.nvmrc +0 -1
  59. package/.prettierignore +0 -3
  60. package/.prettierrc.json +0 -3
  61. package/.releaserc.json +0 -47
  62. package/LICENSE +0 -202
  63. package/commitlint.config.cjs +0 -7
  64. package/generateOptionsFromSpec.ts +0 -68
  65. package/jest/setup.js +0 -5
  66. package/jest.config.ts +0 -199
  67. package/mocks/bin/psql.cjs +0 -9
  68. package/mocks/main/projects/GET.js +0 -27
  69. package/mocks/main/projects/POST.js +0 -22
  70. package/mocks/main/projects/shared/GET.js +0 -16
  71. package/mocks/main/projects/test/DELETE.json +0 -7
  72. package/mocks/main/projects/test/GET.json +0 -13
  73. package/mocks/main/projects/test/PATCH.js +0 -18
  74. package/mocks/main/projects/test/branches/GET.json +0 -25
  75. package/mocks/main/projects/test/branches/POST.js +0 -83
  76. package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/DELETE.json +0 -7
  77. package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/GET.json +0 -9
  78. package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/PATCH.js +0 -14
  79. package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/databases/GET.json +0 -6
  80. package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/databases/POST.js +0 -13
  81. package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/databases/test_db/DELETE.json +0 -6
  82. package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/endpoints/GET.json +0 -26
  83. package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/endpoints/POST.json +0 -6
  84. package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/roles/GET.json +0 -3
  85. package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/roles/POST.js +0 -14
  86. package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/roles/test_role/DELETE.json +0 -6
  87. package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/roles/test_role/reveal_password/GET.json +0 -3
  88. package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/set_as_primary/POST.json +0 -9
  89. package/mocks/main/projects/test/branches/br-numbered-branch-123456/GET.json +0 -10
  90. package/mocks/main/projects/test/branches/br-sunny-branch-123456/DELETE.json +0 -7
  91. package/mocks/main/projects/test/branches/br-sunny-branch-123456/GET.json +0 -10
  92. package/mocks/main/projects/test/branches/br-sunny-branch-123456/PATCH.js +0 -14
  93. package/mocks/main/projects/test/branches/br-sunny-branch-123456/databases/GET.json +0 -6
  94. package/mocks/main/projects/test/branches/br-sunny-branch-123456/databases/POST.js +0 -13
  95. package/mocks/main/projects/test/branches/br-sunny-branch-123456/databases/test_db/DELETE.json +0 -6
  96. package/mocks/main/projects/test/branches/br-sunny-branch-123456/endpoints/GET.json +0 -26
  97. package/mocks/main/projects/test/branches/br-sunny-branch-123456/endpoints/POST.json +0 -6
  98. package/mocks/main/projects/test/branches/br-sunny-branch-123456/restore/POST.js +0 -16
  99. package/mocks/main/projects/test/branches/br-sunny-branch-123456/roles/GET.json +0 -3
  100. package/mocks/main/projects/test/branches/br-sunny-branch-123456/roles/POST.js +0 -14
  101. package/mocks/main/projects/test/branches/br-sunny-branch-123456/roles/test_role/DELETE.json +0 -6
  102. package/mocks/main/projects/test/branches/br-sunny-branch-123456/roles/test_role/reveal_password/GET.json +0 -3
  103. package/mocks/main/projects/test/branches/br-sunny-branch-123456/set_as_primary/POST.json +0 -9
  104. package/mocks/main/projects/test/endpoints/GET.json +0 -9
  105. package/mocks/main/projects/test/endpoints/POST.js +0 -32
  106. package/mocks/main/projects/test/endpoints/test_endpoint_id/DELETE.json +0 -7
  107. package/mocks/main/projects/test/endpoints/test_endpoint_id/GET.json +0 -9
  108. package/mocks/main/projects/test/endpoints/test_endpoint_id/PATCH.js +0 -17
  109. package/mocks/main/projects/test/operations/GET.json +0 -22
  110. package/mocks/main/users/me/GET.json +0 -5
  111. package/mocks/restore/projects/test/branches/GET.json +0 -21
  112. package/mocks/restore/projects/test/branches/br-another-branch-123456/GET.json +0 -6
  113. package/mocks/restore/projects/test/branches/br-another-branch-123456/restore/POST.js +0 -13
  114. package/mocks/restore/projects/test/branches/br-any-branch-123456/GET.json +0 -6
  115. package/mocks/restore/projects/test/branches/br-parent-tots-123456/GET.json +0 -7
  116. package/mocks/restore/projects/test/branches/br-parent-tots-123456/restore/POST.js +0 -14
  117. package/mocks/restore/projects/test/branches/br-self-tolsn-123456/GET.json +0 -6
  118. package/mocks/restore/projects/test/branches/br-self-tolsn-123456/restore/POST.js +0 -15
  119. package/mocks/single_project/projects/GET.json +0 -10
  120. package/mocks/single_project/projects/test-project-123456/GET.json +0 -14
  121. package/mocks/single_project/projects/test-project-123456/branches/GET.json +0 -11
  122. package/mocks/single_project/projects/test-project-123456/branches/br-main-branch-123456/databases/GET.json +0 -3
  123. package/mocks/single_project/projects/test-project-123456/branches/br-main-branch-123456/endpoints/GET.json +0 -10
  124. package/mocks/single_project/projects/test-project-123456/branches/br-main-branch-123456/roles/GET.json +0 -3
  125. package/mocks/single_project/projects/test-project-123456/branches/br-main-branch-123456/roles/test_role/reveal_password/GET.json +0 -3
  126. package/rollup.config.js +0 -20
  127. package/snapshots/commands/branches.test.snap +0 -221
  128. package/snapshots/commands/connection_string.test.snap +0 -70
  129. package/snapshots/commands/databases.test.snap +0 -20
  130. package/snapshots/commands/ip_allow.test.snap +0 -55
  131. package/snapshots/commands/operations.test.snap +0 -17
  132. package/snapshots/commands/projects.test.snap +0 -141
  133. package/snapshots/commands/roles.test.snap +0 -19
  134. package/snapshots/commands/set_context.test.snap +0 -30
  135. package/snapshots/writer.test.snap +0 -60
  136. package/snapshotsResolver.cjs +0 -32
  137. package/src/analytics.ts +0 -95
  138. package/src/api.ts +0 -44
  139. package/src/auth.ts +0 -137
  140. package/src/commands/auth.test.ts +0 -62
  141. package/src/commands/auth.ts +0 -148
  142. package/src/commands/branches.test.ts +0 -354
  143. package/src/commands/branches.ts +0 -451
  144. package/src/commands/connection_string.test.ts +0 -250
  145. package/src/commands/connection_string.ts +0 -210
  146. package/src/commands/databases.test.ts +0 -55
  147. package/src/commands/databases.ts +0 -129
  148. package/src/commands/help.test.ts +0 -13
  149. package/src/commands/ip_allow.test.ts +0 -86
  150. package/src/commands/ip_allow.ts +0 -202
  151. package/src/commands/operations.test.ts +0 -13
  152. package/src/commands/operations.ts +0 -41
  153. package/src/commands/projects.test.ts +0 -147
  154. package/src/commands/projects.ts +0 -275
  155. package/src/commands/roles.test.ts +0 -46
  156. package/src/commands/roles.ts +0 -100
  157. package/src/commands/set_context.test.ts +0 -64
  158. package/src/commands/set_context.ts +0 -27
  159. package/src/commands/user.ts +0 -21
  160. package/src/config.ts +0 -22
  161. package/src/context.ts +0 -61
  162. package/src/env.ts +0 -7
  163. package/src/errors.ts +0 -24
  164. package/src/help.ts +0 -185
  165. package/src/index.ts +0 -180
  166. package/src/log.ts +0 -16
  167. package/src/parameters.gen.ts +0 -332
  168. package/src/pkg.ts +0 -9
  169. package/src/test_utils/mock_server.ts +0 -27
  170. package/src/test_utils/oauth_server.ts +0 -10
  171. package/src/test_utils/test_cli_command.ts +0 -117
  172. package/src/types.ts +0 -25
  173. package/src/utils/enrichers.ts +0 -73
  174. package/src/utils/formats.test.ts +0 -41
  175. package/src/utils/formats.ts +0 -11
  176. package/src/utils/middlewares.ts +0 -23
  177. package/src/utils/point_in_time.ts +0 -86
  178. package/src/utils/psql.ts +0 -29
  179. package/src/utils/string.ts +0 -8
  180. package/src/utils/ui.ts +0 -64
  181. package/src/writer.test.ts +0 -98
  182. package/src/writer.ts +0 -131
  183. package/tsconfig.json +0 -17
  184. /package/{src/callback.html → callback.html} +0 -0
@@ -1,32 +0,0 @@
1
- const path = require('path');
2
-
3
- module.exports = {
4
- // resolves from test to snapshot path
5
- resolveSnapshotPath: (testPath, snapshotExtension) => {
6
- const p = path.relative(__dirname, testPath);
7
- const parts = p.split(path.sep);
8
- parts[0] = 'snapshots';
9
- parts[parts.length - 1] = parts[parts.length - 1].replace(
10
- '.js',
11
- snapshotExtension,
12
- );
13
- const r = path.join(__dirname, ...parts);
14
- return r;
15
- },
16
-
17
- // resolves from snapshot to test path
18
- resolveTestPath: (snapshotFilePath, snapshotExtension) => {
19
- const p = path.relative(__dirname, snapshotFilePath);
20
- const parts = p.split(path.sep);
21
- parts[0] = 'dist';
22
- parts[parts.length - 1] = parts[parts.length - 1].replace(
23
- snapshotExtension,
24
- '.js',
25
- );
26
- const r = path.join(__dirname, ...parts);
27
- return r;
28
- },
29
-
30
- // Example test path, used for preflight consistency check of the implementation above
31
- testPathForConsistencyCheck: `${__dirname}/dist/example.test.js`,
32
- };
package/src/analytics.ts DELETED
@@ -1,95 +0,0 @@
1
- import { readFileSync } from 'node:fs';
2
- import { join } from 'node:path';
3
-
4
- import { Analytics } from '@segment/analytics-node';
5
- import { isAxiosError } from 'axios';
6
-
7
- import { CREDENTIALS_FILE } from './config.js';
8
- import { isCi } from './env.js';
9
- import { ErrorCode } from './errors.js';
10
- import { log } from './log.js';
11
- import pkg from './pkg.js';
12
- import { getApiClient } from './api.js';
13
-
14
- const WRITE_KEY = '3SQXn5ejjXWLEJ8xU2PRYhAotLtTaeeV';
15
-
16
- let client: Analytics | undefined;
17
- let userId = '';
18
-
19
- export const analyticsMiddleware = async (args: {
20
- analytics: boolean;
21
- apiKey?: string;
22
- apiHost?: string;
23
- configDir: string;
24
- _: (string | number)[];
25
- [key: string]: unknown;
26
- }) => {
27
- if (!args.analytics) {
28
- return;
29
- }
30
-
31
- try {
32
- const credentialsPath = join(args.configDir, CREDENTIALS_FILE);
33
- const credentials = readFileSync(credentialsPath, { encoding: 'utf-8' });
34
- userId = JSON.parse(credentials).user_id;
35
- } catch (err) {
36
- log.debug('Failed to read credentials file', err);
37
- }
38
-
39
- try {
40
- if (!userId && args.apiKey) {
41
- const apiClient = getApiClient({
42
- apiKey: args.apiKey,
43
- apiHost: args.apiHost,
44
- });
45
- const resp = await apiClient?.getCurrentUserInfo?.();
46
- userId = resp?.data?.id;
47
- }
48
- } catch (err) {
49
- log.debug('Failed to get user id from api', err);
50
- }
51
-
52
- client = new Analytics({
53
- writeKey: WRITE_KEY,
54
- host: 'https://track.neon.tech',
55
- });
56
-
57
- client.identify({
58
- userId: userId?.toString() ?? 'anonymous',
59
- });
60
-
61
- client.track({
62
- userId: userId ?? 'anonymous',
63
- event: 'CLI Started',
64
- properties: {
65
- version: pkg.version,
66
- command: args._.join(' '),
67
- flags: {
68
- output: args.output,
69
- },
70
- ci: isCi(),
71
- },
72
- });
73
- log.debug('Flushing CLI started event with userId: %s', userId);
74
- await client.closeAndFlush();
75
- log.debug('Flushed CLI started event with userId: %s', userId);
76
- };
77
-
78
- export const sendError = (err: Error, errCode: ErrorCode) => {
79
- if (!client) {
80
- return;
81
- }
82
- const axiosError = isAxiosError(err) ? err : undefined;
83
- client.track({
84
- event: 'CLI Error',
85
- userId: userId ?? 'anonymous',
86
- properties: {
87
- message: err.message,
88
- stack: err.stack,
89
- errCode,
90
- statusCode: axiosError?.response?.status,
91
- },
92
- });
93
- client.closeAndFlush();
94
- log.debug('Sent CLI error event: %s', errCode);
95
- };
package/src/api.ts DELETED
@@ -1,44 +0,0 @@
1
- import { createApiClient } from '@neondatabase/api-client';
2
- import { isAxiosError } from 'axios';
3
-
4
- import { log } from './log.js';
5
- import pkg from './pkg.js';
6
-
7
- export type ApiCallProps = {
8
- apiKey: string;
9
- apiHost?: string;
10
- };
11
-
12
- export const getApiClient = ({ apiKey, apiHost }: ApiCallProps) =>
13
- createApiClient({
14
- apiKey,
15
- baseURL: apiHost,
16
- timeout: 60000,
17
- headers: {
18
- 'User-Agent': `neonctl v${pkg.version}`,
19
- },
20
- });
21
-
22
- const RETRY_COUNT = 5;
23
- const RETRY_DELAY = 3000;
24
- export const retryOnLock = async <T>(fn: () => Promise<T>): Promise<T> => {
25
- let attempt = 0;
26
- let errOut: unknown;
27
- while (attempt < RETRY_COUNT) {
28
- try {
29
- return await fn();
30
- } catch (err) {
31
- errOut = err;
32
- if (isAxiosError(err) && err.response?.status === 423) {
33
- attempt++;
34
- log.info(
35
- `Resource is locked. Waiting ${RETRY_DELAY}ms before retrying...`,
36
- );
37
- await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY));
38
- } else {
39
- throw err;
40
- }
41
- }
42
- }
43
- throw errOut;
44
- };
package/src/auth.ts DELETED
@@ -1,137 +0,0 @@
1
- import { custom, generators, Issuer, TokenSet } 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
-
7
- import { log } from './log.js';
8
- import { AddressInfo } from 'node:net';
9
- import { fileURLToPath } from 'node:url';
10
-
11
- // oauth server timeouts
12
- const SERVER_TIMEOUT = 10_000;
13
- // where to wait for incoming redirect request from oauth server to arrive
14
- const REDIRECT_URI = (port: number) => `http://127.0.0.1:${port}/callback`;
15
- // These scopes cannot be cancelled, they are always needed.
16
- const ALWAYS_PRESENT_SCOPES = ['openid', 'offline', 'offline_access'] as const;
17
-
18
- const NEONCTL_SCOPES = [
19
- ...ALWAYS_PRESENT_SCOPES,
20
- 'urn:neoncloud:projects:create',
21
- 'urn:neoncloud:projects:read',
22
- 'urn:neoncloud:projects:update',
23
- 'urn:neoncloud:projects:delete',
24
- ] as const;
25
-
26
- export const defaultClientID = 'neonctl';
27
-
28
- export type AuthProps = {
29
- oauthHost: string;
30
- clientId: string;
31
- };
32
-
33
- custom.setHttpOptionsDefaults({
34
- timeout: SERVER_TIMEOUT,
35
- });
36
-
37
- export const refreshToken = async (
38
- { oauthHost, clientId }: AuthProps,
39
- tokenSet: TokenSet,
40
- ) => {
41
- log.debug('Discovering oauth server');
42
- const issuer = await Issuer.discover(oauthHost);
43
-
44
- const neonOAuthClient = new issuer.Client({
45
- token_endpoint_auth_method: 'none',
46
- client_id: clientId,
47
- response_types: ['code'],
48
- });
49
- return await neonOAuthClient.refresh(tokenSet);
50
- };
51
-
52
- export const auth = async ({ oauthHost, clientId }: AuthProps) => {
53
- log.debug('Discovering oauth server');
54
- const issuer = await Issuer.discover(oauthHost);
55
-
56
- //
57
- // Start HTTP server and wait till /callback is hit
58
- //
59
- const server = createServer();
60
- server.listen(0, '127.0.0.1', function (this: typeof server) {
61
- log.debug(`Listening on port ${(this.address() as AddressInfo).port}`);
62
- });
63
- await new Promise((resolve) => server.once('listening', resolve));
64
- const listen_port = (server.address() as AddressInfo).port;
65
-
66
- const neonOAuthClient = new issuer.Client({
67
- token_endpoint_auth_method: 'none',
68
- client_id: clientId,
69
- redirect_uris: [REDIRECT_URI(listen_port)],
70
- response_types: ['code'],
71
- });
72
-
73
- // https://datatracker.ietf.org/doc/html/rfc6819#section-4.4.1.8
74
- const state = generators.state();
75
-
76
- // we store the code_verifier in memory
77
- const codeVerifier = generators.codeVerifier();
78
-
79
- const codeChallenge = generators.codeChallenge(codeVerifier);
80
-
81
- return new Promise<TokenSet>((resolve) => {
82
- server.on('request', async (request, response) => {
83
- //
84
- // Wait for callback and follow oauth flow.
85
- //
86
- if (!request.url?.startsWith('/callback')) {
87
- response.writeHead(404);
88
- response.end();
89
- return;
90
- }
91
-
92
- // process the CORS preflight OPTIONS request
93
- if (request.method === 'OPTIONS') {
94
- response.writeHead(200, {
95
- 'Access-Control-Allow-Origin': '*',
96
- 'Access-Control-Allow-Methods': 'GET, POST',
97
- 'Access-Control-Allow-Headers': 'Content-Type',
98
- });
99
- response.end();
100
- return;
101
- }
102
-
103
- log.debug(`Callback received: ${request.url}`);
104
- const params = neonOAuthClient.callbackParams(request);
105
- const tokenSet = await neonOAuthClient.callback(
106
- REDIRECT_URI(listen_port),
107
- params,
108
- {
109
- code_verifier: codeVerifier,
110
- state,
111
- },
112
- );
113
-
114
- response.writeHead(200, { 'Content-Type': 'text/html' });
115
- createReadStream(
116
- join(fileURLToPath(new URL('.', import.meta.url)), './callback.html'),
117
- ).pipe(response);
118
- resolve(tokenSet);
119
- server.close();
120
- });
121
-
122
- //
123
- // Open browser to let user authenticate
124
- //
125
- const scopes =
126
- clientId == defaultClientID ? NEONCTL_SCOPES : ALWAYS_PRESENT_SCOPES;
127
-
128
- const authUrl = neonOAuthClient.authorizationUrl({
129
- scope: scopes.join(' '),
130
- state,
131
- code_challenge: codeChallenge,
132
- code_challenge_method: 'S256',
133
- });
134
-
135
- open(authUrl);
136
- });
137
- };
@@ -1,62 +0,0 @@
1
- import axios from 'axios';
2
- import {
3
- beforeAll,
4
- describe,
5
- test,
6
- jest,
7
- afterAll,
8
- expect,
9
- } from '@jest/globals';
10
- import { AddressInfo } from 'node:net';
11
- import { mkdtempSync, rmSync, readFileSync } from 'node:fs';
12
- import { Server } from 'node:http';
13
-
14
- import { startOauthServer } from '../test_utils/oauth_server';
15
- import { OAuth2Server } from 'oauth2-mock-server';
16
- import { runMockServer } from '../test_utils/mock_server';
17
-
18
- jest.unstable_mockModule('open', () => ({
19
- __esModule: true,
20
- default: jest.fn((url: string) => {
21
- axios.get(url);
22
- }),
23
- }));
24
-
25
- // "open" module should be imported after mocking
26
- const authModule = await import('./auth');
27
-
28
- describe('auth', () => {
29
- let configDir = '';
30
- let oauthServer: OAuth2Server;
31
- let mockServer: Server;
32
-
33
- beforeAll(async () => {
34
- configDir = mkdtempSync('test-config');
35
- oauthServer = await startOauthServer();
36
- mockServer = await runMockServer('main');
37
- });
38
-
39
- afterAll(async () => {
40
- rmSync(configDir, { recursive: true });
41
- await oauthServer.stop();
42
- await new Promise((resolve) => mockServer.close(resolve));
43
- });
44
-
45
- test('should auth', async () => {
46
- await authModule.authFlow({
47
- _: ['auth'],
48
- apiHost: `http://localhost:${(mockServer.address() as AddressInfo).port}`,
49
- clientId: 'test-client-id',
50
- configDir,
51
- forceAuth: true,
52
- oauthHost: `http://localhost:${oauthServer.address().port}`,
53
- });
54
-
55
- const credentials = JSON.parse(
56
- readFileSync(`${configDir}/credentials.json`, 'utf-8'),
57
- );
58
- expect(credentials.access_token).toEqual(expect.any(String));
59
- expect(credentials.refresh_token).toEqual(expect.any(String));
60
- expect(credentials.user_id).toEqual(expect.any(String));
61
- });
62
- });
@@ -1,148 +0,0 @@
1
- import { join } from 'node:path';
2
- import { writeFileSync, existsSync, readFileSync } from 'node:fs';
3
- import { TokenSet } from 'openid-client';
4
- import yargs from 'yargs';
5
-
6
- import { Api } from '@neondatabase/api-client';
7
-
8
- import { auth, refreshToken } from '../auth.js';
9
- import { log } from '../log.js';
10
- import { getApiClient } from '../api.js';
11
- import { isCi } from '../env.js';
12
- import { CREDENTIALS_FILE } from '../config.js';
13
-
14
- type AuthProps = {
15
- _: (string | number)[];
16
- configDir: string;
17
- oauthHost: string;
18
- apiHost: string;
19
- clientId: string;
20
- forceAuth: boolean;
21
- };
22
-
23
- export const command = 'auth';
24
- export const aliases = ['login'];
25
- export const describe = 'Authenticate';
26
- export const builder = (yargs: yargs.Argv) =>
27
- yargs.option('context-file', {
28
- hidden: true,
29
- });
30
- export const handler = async (args: AuthProps) => {
31
- await authFlow(args);
32
- };
33
-
34
- export const authFlow = async ({
35
- configDir,
36
- oauthHost,
37
- clientId,
38
- apiHost,
39
- forceAuth,
40
- }: AuthProps) => {
41
- if (!forceAuth && isCi()) {
42
- throw new Error('Cannot run interactive auth in CI');
43
- }
44
- const tokenSet = await auth({
45
- oauthHost: oauthHost,
46
- clientId: clientId,
47
- });
48
-
49
- const credentialsPath = join(configDir, CREDENTIALS_FILE);
50
- await preserveCredentials(
51
- credentialsPath,
52
- tokenSet,
53
- getApiClient({
54
- apiKey: tokenSet.access_token || '',
55
- apiHost,
56
- }),
57
- );
58
- log.info(`Saved credentials to ${credentialsPath}`);
59
- log.info('Auth complete');
60
- return tokenSet.access_token || '';
61
- };
62
-
63
- const preserveCredentials = async (
64
- path: string,
65
- credentials: TokenSet,
66
- apiClient: Api<unknown>,
67
- ) => {
68
- const {
69
- data: { id },
70
- } = await apiClient.getCurrentUserInfo();
71
- const contents = JSON.stringify({
72
- ...credentials,
73
- user_id: id,
74
- });
75
- // correctly sets needed permissions for the credentials file
76
- writeFileSync(path, contents, {
77
- mode: 0o700,
78
- });
79
- };
80
-
81
- export const ensureAuth = async (
82
- props: AuthProps & { apiKey: string; apiClient: Api<unknown>; help: boolean },
83
- ) => {
84
- if (props._.length === 0 || props.help) {
85
- return;
86
- }
87
- if (props.apiKey || props._[0] === 'auth') {
88
- props.apiClient = getApiClient({
89
- apiKey: props.apiKey,
90
- apiHost: props.apiHost,
91
- });
92
- return;
93
- }
94
- const credentialsPath = join(props.configDir, CREDENTIALS_FILE);
95
- if (existsSync(credentialsPath)) {
96
- try {
97
- const tokenSetContents = await JSON.parse(
98
- readFileSync(credentialsPath, 'utf8'),
99
- );
100
- const tokenSet = new TokenSet(tokenSetContents);
101
- if (tokenSet.expired()) {
102
- log.debug('using refresh token to update access token');
103
- const refreshedTokenSet = await refreshToken(
104
- {
105
- oauthHost: props.oauthHost,
106
- clientId: props.clientId,
107
- },
108
- tokenSet,
109
- ).catch((e) => {
110
- log.error('failed to refresh token\n%s', e?.message);
111
- process.exit(1);
112
- });
113
-
114
- props.apiKey = refreshedTokenSet.access_token || 'UNKNOWN';
115
- props.apiClient = getApiClient({
116
- apiKey: props.apiKey,
117
- apiHost: props.apiHost,
118
- });
119
- await preserveCredentials(
120
- credentialsPath,
121
- refreshedTokenSet,
122
- props.apiClient,
123
- );
124
- return;
125
- }
126
- const token = tokenSet.access_token || 'UNKNOWN';
127
-
128
- props.apiKey = token;
129
- props.apiClient = getApiClient({
130
- apiKey: props.apiKey,
131
- apiHost: props.apiHost,
132
- });
133
- return;
134
- } catch (e) {
135
- if ((e as { code: string }).code !== 'ENOENT') {
136
- // not a "file does not exist" error
137
- throw e;
138
- }
139
- props.apiKey = await authFlow(props);
140
- }
141
- } else {
142
- props.apiKey = await authFlow(props);
143
- }
144
- props.apiClient = getApiClient({
145
- apiKey: props.apiKey,
146
- apiHost: props.apiHost,
147
- });
148
- };