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.
- package/analytics.js +78 -0
- package/api.js +35 -0
- package/auth.js +101 -0
- package/{src/cli.ts → cli.js} +0 -1
- package/commands/auth.js +102 -0
- package/commands/auth.test.js +42 -0
- package/commands/branches.js +303 -0
- package/commands/branches.test.js +321 -0
- package/commands/connection_string.js +156 -0
- package/commands/connection_string.test.js +236 -0
- package/commands/databases.js +79 -0
- package/commands/databases.test.js +51 -0
- package/commands/help.test.js +11 -0
- package/{src/commands/index.ts → commands/index.js} +10 -11
- package/commands/ip_allow.js +135 -0
- package/commands/ip_allow.test.js +78 -0
- package/commands/operations.js +28 -0
- package/commands/operations.test.js +11 -0
- package/commands/projects.js +186 -0
- package/commands/projects.test.js +132 -0
- package/commands/roles.js +57 -0
- package/commands/roles.test.js +42 -0
- package/commands/set_context.js +22 -0
- package/commands/set_context.test.js +53 -0
- package/commands/user.js +15 -0
- package/config.js +11 -0
- package/context.js +48 -0
- package/env.js +6 -0
- package/errors.js +16 -0
- package/help.js +146 -0
- package/index.js +168 -0
- package/log.js +15 -0
- package/package.json +1 -1
- package/parameters.gen.js +322 -0
- package/pkg.js +3 -45
- package/test_utils/mock_server.js +16 -0
- package/test_utils/oauth_server.js +9 -0
- package/test_utils/test_cli_command.js +80 -0
- package/types.js +1 -0
- package/utils/enrichers.js +49 -0
- package/utils/formats.js +5 -0
- package/utils/formats.test.js +32 -0
- package/utils/middlewares.js +20 -0
- package/utils/point_in_time.js +50 -0
- package/utils/psql.js +24 -0
- package/utils/string.js +5 -0
- package/utils/ui.js +59 -0
- package/writer.js +87 -0
- package/writer.test.js +86 -0
- package/.bump +0 -1
- package/.editorconfig +0 -7
- package/.eslintrc.cjs +0 -15
- package/.github/workflows/commitlint.yml +0 -46
- package/.github/workflows/pr.yml +0 -25
- package/.github/workflows/release.yml +0 -30
- package/.husky/commit-msg +0 -4
- package/.husky/pre-commit +0 -4
- package/.nvmrc +0 -1
- package/.prettierignore +0 -3
- package/.prettierrc.json +0 -3
- package/.releaserc.json +0 -47
- package/LICENSE +0 -202
- package/commitlint.config.cjs +0 -7
- package/generateOptionsFromSpec.ts +0 -68
- package/jest/setup.js +0 -5
- package/jest.config.ts +0 -199
- package/mocks/bin/psql.cjs +0 -9
- package/mocks/main/projects/GET.js +0 -27
- package/mocks/main/projects/POST.js +0 -22
- package/mocks/main/projects/shared/GET.js +0 -16
- package/mocks/main/projects/test/DELETE.json +0 -7
- package/mocks/main/projects/test/GET.json +0 -13
- package/mocks/main/projects/test/PATCH.js +0 -18
- package/mocks/main/projects/test/branches/GET.json +0 -25
- package/mocks/main/projects/test/branches/POST.js +0 -83
- package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/DELETE.json +0 -7
- package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/GET.json +0 -9
- package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/PATCH.js +0 -14
- package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/databases/GET.json +0 -6
- package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/databases/POST.js +0 -13
- package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/databases/test_db/DELETE.json +0 -6
- package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/endpoints/GET.json +0 -26
- package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/endpoints/POST.json +0 -6
- package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/roles/GET.json +0 -3
- package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/roles/POST.js +0 -14
- package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/roles/test_role/DELETE.json +0 -6
- package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/roles/test_role/reveal_password/GET.json +0 -3
- package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/set_as_primary/POST.json +0 -9
- package/mocks/main/projects/test/branches/br-numbered-branch-123456/GET.json +0 -10
- package/mocks/main/projects/test/branches/br-sunny-branch-123456/DELETE.json +0 -7
- package/mocks/main/projects/test/branches/br-sunny-branch-123456/GET.json +0 -10
- package/mocks/main/projects/test/branches/br-sunny-branch-123456/PATCH.js +0 -14
- package/mocks/main/projects/test/branches/br-sunny-branch-123456/databases/GET.json +0 -6
- package/mocks/main/projects/test/branches/br-sunny-branch-123456/databases/POST.js +0 -13
- package/mocks/main/projects/test/branches/br-sunny-branch-123456/databases/test_db/DELETE.json +0 -6
- package/mocks/main/projects/test/branches/br-sunny-branch-123456/endpoints/GET.json +0 -26
- package/mocks/main/projects/test/branches/br-sunny-branch-123456/endpoints/POST.json +0 -6
- package/mocks/main/projects/test/branches/br-sunny-branch-123456/restore/POST.js +0 -16
- package/mocks/main/projects/test/branches/br-sunny-branch-123456/roles/GET.json +0 -3
- package/mocks/main/projects/test/branches/br-sunny-branch-123456/roles/POST.js +0 -14
- package/mocks/main/projects/test/branches/br-sunny-branch-123456/roles/test_role/DELETE.json +0 -6
- package/mocks/main/projects/test/branches/br-sunny-branch-123456/roles/test_role/reveal_password/GET.json +0 -3
- package/mocks/main/projects/test/branches/br-sunny-branch-123456/set_as_primary/POST.json +0 -9
- package/mocks/main/projects/test/endpoints/GET.json +0 -9
- package/mocks/main/projects/test/endpoints/POST.js +0 -32
- package/mocks/main/projects/test/endpoints/test_endpoint_id/DELETE.json +0 -7
- package/mocks/main/projects/test/endpoints/test_endpoint_id/GET.json +0 -9
- package/mocks/main/projects/test/endpoints/test_endpoint_id/PATCH.js +0 -17
- package/mocks/main/projects/test/operations/GET.json +0 -22
- package/mocks/main/users/me/GET.json +0 -5
- package/mocks/restore/projects/test/branches/GET.json +0 -21
- package/mocks/restore/projects/test/branches/br-another-branch-123456/GET.json +0 -6
- package/mocks/restore/projects/test/branches/br-another-branch-123456/restore/POST.js +0 -13
- package/mocks/restore/projects/test/branches/br-any-branch-123456/GET.json +0 -6
- package/mocks/restore/projects/test/branches/br-parent-tots-123456/GET.json +0 -7
- package/mocks/restore/projects/test/branches/br-parent-tots-123456/restore/POST.js +0 -14
- package/mocks/restore/projects/test/branches/br-self-tolsn-123456/GET.json +0 -6
- package/mocks/restore/projects/test/branches/br-self-tolsn-123456/restore/POST.js +0 -15
- package/mocks/single_project/projects/GET.json +0 -10
- package/mocks/single_project/projects/test-project-123456/GET.json +0 -14
- package/mocks/single_project/projects/test-project-123456/branches/GET.json +0 -11
- package/mocks/single_project/projects/test-project-123456/branches/br-main-branch-123456/databases/GET.json +0 -3
- package/mocks/single_project/projects/test-project-123456/branches/br-main-branch-123456/endpoints/GET.json +0 -10
- package/mocks/single_project/projects/test-project-123456/branches/br-main-branch-123456/roles/GET.json +0 -3
- package/mocks/single_project/projects/test-project-123456/branches/br-main-branch-123456/roles/test_role/reveal_password/GET.json +0 -3
- package/rollup.config.js +0 -20
- package/snapshots/commands/branches.test.snap +0 -221
- package/snapshots/commands/connection_string.test.snap +0 -70
- package/snapshots/commands/databases.test.snap +0 -20
- package/snapshots/commands/ip_allow.test.snap +0 -55
- package/snapshots/commands/operations.test.snap +0 -17
- package/snapshots/commands/projects.test.snap +0 -141
- package/snapshots/commands/roles.test.snap +0 -19
- package/snapshots/commands/set_context.test.snap +0 -30
- package/snapshots/writer.test.snap +0 -60
- package/snapshotsResolver.cjs +0 -32
- package/src/analytics.ts +0 -95
- package/src/api.ts +0 -44
- package/src/auth.ts +0 -137
- package/src/commands/auth.test.ts +0 -62
- package/src/commands/auth.ts +0 -148
- package/src/commands/branches.test.ts +0 -354
- package/src/commands/branches.ts +0 -451
- package/src/commands/connection_string.test.ts +0 -250
- package/src/commands/connection_string.ts +0 -210
- package/src/commands/databases.test.ts +0 -55
- package/src/commands/databases.ts +0 -129
- package/src/commands/help.test.ts +0 -13
- package/src/commands/ip_allow.test.ts +0 -86
- package/src/commands/ip_allow.ts +0 -202
- package/src/commands/operations.test.ts +0 -13
- package/src/commands/operations.ts +0 -41
- package/src/commands/projects.test.ts +0 -147
- package/src/commands/projects.ts +0 -275
- package/src/commands/roles.test.ts +0 -46
- package/src/commands/roles.ts +0 -100
- package/src/commands/set_context.test.ts +0 -64
- package/src/commands/set_context.ts +0 -27
- package/src/commands/user.ts +0 -21
- package/src/config.ts +0 -22
- package/src/context.ts +0 -61
- package/src/env.ts +0 -7
- package/src/errors.ts +0 -24
- package/src/help.ts +0 -185
- package/src/index.ts +0 -180
- package/src/log.ts +0 -16
- package/src/parameters.gen.ts +0 -332
- package/src/pkg.ts +0 -9
- package/src/test_utils/mock_server.ts +0 -27
- package/src/test_utils/oauth_server.ts +0 -10
- package/src/test_utils/test_cli_command.ts +0 -117
- package/src/types.ts +0 -25
- package/src/utils/enrichers.ts +0 -73
- package/src/utils/formats.test.ts +0 -41
- package/src/utils/formats.ts +0 -11
- package/src/utils/middlewares.ts +0 -23
- package/src/utils/point_in_time.ts +0 -86
- package/src/utils/psql.ts +0 -29
- package/src/utils/string.ts +0 -8
- package/src/utils/ui.ts +0 -64
- package/src/writer.test.ts +0 -98
- package/src/writer.ts +0 -131
- package/tsconfig.json +0 -17
- /package/{src/callback.html → callback.html} +0 -0
package/snapshotsResolver.cjs
DELETED
|
@@ -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
|
-
});
|
package/src/commands/auth.ts
DELETED
|
@@ -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
|
-
};
|