neonctl 2.15.0 → 2.16.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/auth.js +26 -29
- package/commands/auth.js +13 -18
- package/commands/auth.test.js +19 -18
- package/commands/index.js +2 -0
- package/commands/init.js +44 -0
- package/commands/init.test.js +10 -0
- package/index.js +1 -0
- package/package.json +4 -3
- package/test_utils/oauth_server.js +1 -1
- package/utils/auth.js +5 -0
- package/utils/formats.js +15 -2
package/auth.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as client from 'openid-client';
|
|
2
2
|
import { createServer } from 'node:http';
|
|
3
3
|
import { createReadStream } from 'node:fs';
|
|
4
4
|
import { join } from 'node:path';
|
|
@@ -7,6 +7,7 @@ import { log } from './log.js';
|
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
8
|
import { sendError } from './analytics.js';
|
|
9
9
|
import { matchErrorCode } from './errors.js';
|
|
10
|
+
import { extendTokenSet } from './utils/auth.js';
|
|
10
11
|
// oauth server timeouts
|
|
11
12
|
const SERVER_TIMEOUT = 10000;
|
|
12
13
|
// where to wait for incoming redirect request from oauth server to arrive
|
|
@@ -27,22 +28,22 @@ const NEONCTL_SCOPES = [
|
|
|
27
28
|
];
|
|
28
29
|
const AUTH_TIMEOUT_SECONDS = 60;
|
|
29
30
|
export const defaultClientID = 'neonctl';
|
|
30
|
-
|
|
31
|
-
timeout: SERVER_TIMEOUT,
|
|
32
|
-
});
|
|
33
|
-
export const refreshToken = async ({ oauthHost, clientId }, tokenSet) => {
|
|
31
|
+
export const refreshToken = async ({ oauthHost, clientId, allowUnsafeTls }, tokenSet) => {
|
|
34
32
|
log.debug('Discovering oauth server');
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
response_types: ['code'],
|
|
33
|
+
const configuration = await client.discovery(new URL(oauthHost), clientId, { token_endpoint_auth_method: 'none' }, client.None(), {
|
|
34
|
+
timeout: SERVER_TIMEOUT,
|
|
35
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
36
|
+
execute: allowUnsafeTls ? [client.allowInsecureRequests] : undefined,
|
|
40
37
|
});
|
|
41
|
-
return await
|
|
38
|
+
return await client.refreshTokenGrant(configuration, tokenSet.refresh_token);
|
|
42
39
|
};
|
|
43
|
-
export const auth = async ({ oauthHost, clientId }) => {
|
|
40
|
+
export const auth = async ({ oauthHost, clientId, allowUnsafeTls, }) => {
|
|
44
41
|
log.debug('Discovering oauth server');
|
|
45
|
-
const
|
|
42
|
+
const configuration = await client.discovery(new URL(oauthHost), clientId, { token_endpoint_auth_method: 'none' }, client.None(), {
|
|
43
|
+
timeout: SERVER_TIMEOUT,
|
|
44
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
45
|
+
execute: allowUnsafeTls ? [client.allowInsecureRequests] : undefined,
|
|
46
|
+
});
|
|
46
47
|
//
|
|
47
48
|
// Start HTTP server and wait till /callback is hit
|
|
48
49
|
//
|
|
@@ -53,17 +54,11 @@ export const auth = async ({ oauthHost, clientId }) => {
|
|
|
53
54
|
});
|
|
54
55
|
await new Promise((resolve) => server.once('listening', resolve));
|
|
55
56
|
const listen_port = server.address().port;
|
|
56
|
-
const neonOAuthClient = new issuer.Client({
|
|
57
|
-
token_endpoint_auth_method: 'none',
|
|
58
|
-
client_id: clientId,
|
|
59
|
-
redirect_uris: [REDIRECT_URI(listen_port)],
|
|
60
|
-
response_types: ['code'],
|
|
61
|
-
});
|
|
62
57
|
// https://datatracker.ietf.org/doc/html/rfc6819#section-4.4.1.8
|
|
63
|
-
const state =
|
|
58
|
+
const state = client.randomState();
|
|
64
59
|
// we store the code_verifier in memory
|
|
65
|
-
const codeVerifier =
|
|
66
|
-
const codeChallenge =
|
|
60
|
+
const codeVerifier = client.randomPKCECodeVerifier();
|
|
61
|
+
const codeChallenge = await client.calculatePKCECodeChallenge(codeVerifier);
|
|
67
62
|
return new Promise((resolve, reject) => {
|
|
68
63
|
const timer = setTimeout(() => {
|
|
69
64
|
reject(new Error(`Authentication timed out after ${AUTH_TIMEOUT_SECONDS} seconds`));
|
|
@@ -88,15 +83,16 @@ export const auth = async ({ oauthHost, clientId }) => {
|
|
|
88
83
|
return;
|
|
89
84
|
}
|
|
90
85
|
log.debug(`Callback received: ${request.url}`);
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
state,
|
|
86
|
+
const tokenSet = await client.authorizationCodeGrant(configuration, new URL(request.url, `http://127.0.0.1:${listen_port}`), {
|
|
87
|
+
pkceCodeVerifier: codeVerifier,
|
|
88
|
+
expectedState: state,
|
|
95
89
|
});
|
|
96
90
|
response.writeHead(200, { 'Content-Type': 'text/html' });
|
|
97
91
|
createReadStream(join(fileURLToPath(new URL('.', import.meta.url)), './callback.html')).pipe(response);
|
|
98
92
|
clearTimeout(timer);
|
|
99
|
-
|
|
93
|
+
const exp = new Date();
|
|
94
|
+
exp.setSeconds(exp.getSeconds() + (tokenSet.expires_in ?? 0));
|
|
95
|
+
resolve(extendTokenSet(tokenSet));
|
|
100
96
|
server.close();
|
|
101
97
|
};
|
|
102
98
|
server.on('request', (req, res) => {
|
|
@@ -106,15 +102,16 @@ export const auth = async ({ oauthHost, clientId }) => {
|
|
|
106
102
|
// Open browser to let user authenticate
|
|
107
103
|
//
|
|
108
104
|
const scopes = clientId == defaultClientID ? NEONCTL_SCOPES : ALWAYS_PRESENT_SCOPES;
|
|
109
|
-
const authUrl =
|
|
105
|
+
const authUrl = client.buildAuthorizationUrl(configuration, {
|
|
110
106
|
scope: scopes.join(' '),
|
|
111
107
|
state,
|
|
112
108
|
code_challenge: codeChallenge,
|
|
113
109
|
code_challenge_method: 'S256',
|
|
110
|
+
redirect_uri: REDIRECT_URI(listen_port),
|
|
114
111
|
});
|
|
115
112
|
log.info('Awaiting authentication in web browser.');
|
|
116
113
|
log.info(`Auth Url: ${authUrl}`);
|
|
117
|
-
open(authUrl).catch((err) => {
|
|
114
|
+
open(authUrl.href).catch((err) => {
|
|
118
115
|
const msg = `Failed to open web browser. Please copy & paste auth url to authenticate in browser.`;
|
|
119
116
|
const typedErr = err && err instanceof Error ? err : undefined;
|
|
120
117
|
sendError(typedErr || new Error(msg), matchErrorCode(msg));
|
package/commands/auth.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync, rmSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
|
-
import { TokenSet } from 'openid-client';
|
|
5
4
|
import { getApiClient } from '../api.js';
|
|
6
5
|
import { auth, refreshToken } from '../auth.js';
|
|
7
6
|
import { CREDENTIALS_FILE } from '../config.js';
|
|
8
7
|
import { isCi } from '../env.js';
|
|
9
8
|
import { log } from '../log.js';
|
|
9
|
+
import { extendTokenSet } from '../utils/auth.js';
|
|
10
10
|
export const command = 'auth';
|
|
11
11
|
export const aliases = ['login'];
|
|
12
12
|
export const describe = 'Authenticate';
|
|
@@ -16,13 +16,14 @@ export const builder = (yargs) => yargs.option('context-file', {
|
|
|
16
16
|
export const handler = async (args) => {
|
|
17
17
|
await authFlow(args);
|
|
18
18
|
};
|
|
19
|
-
export const authFlow = async ({ configDir, oauthHost, clientId, apiHost, forceAuth, }) => {
|
|
19
|
+
export const authFlow = async ({ configDir, oauthHost, clientId, apiHost, forceAuth, allowUnsafeTls, }) => {
|
|
20
20
|
if (!forceAuth && isCi()) {
|
|
21
21
|
throw new Error('Cannot run interactive auth in CI');
|
|
22
22
|
}
|
|
23
23
|
const tokenSet = await auth({
|
|
24
24
|
oauthHost: oauthHost,
|
|
25
25
|
clientId: clientId,
|
|
26
|
+
allowUnsafeTls,
|
|
26
27
|
});
|
|
27
28
|
const credentialsPath = join(configDir, CREDENTIALS_FILE);
|
|
28
29
|
try {
|
|
@@ -49,17 +50,13 @@ const preserveCredentials = async (path, credentials, apiClient) => {
|
|
|
49
50
|
writeFileSync(path, contents, {
|
|
50
51
|
mode: 0o700,
|
|
51
52
|
});
|
|
52
|
-
log.
|
|
53
|
+
log.debug('Saved credentials to %s', path);
|
|
53
54
|
log.debug('Credentials MD5 hash: %s', md5hash(contents));
|
|
54
55
|
};
|
|
55
|
-
const isCompleteTokenSet = (tokenSet) => {
|
|
56
|
-
return !!(tokenSet.access_token &&
|
|
57
|
-
tokenSet.refresh_token &&
|
|
58
|
-
tokenSet.expires_at);
|
|
59
|
-
};
|
|
60
56
|
const handleExistingToken = async (tokenSet, props, credentialsPath) => {
|
|
61
57
|
// Use existing access_token, if present and valid
|
|
62
|
-
if (
|
|
58
|
+
if (tokenSet.access_token && tokenSet.expires_at > Date.now()) {
|
|
59
|
+
log.debug('Using existing valid access_token');
|
|
63
60
|
const apiClient = getApiClient({
|
|
64
61
|
apiKey: tokenSet.access_token,
|
|
65
62
|
apiHost: props.apiHost,
|
|
@@ -67,7 +64,7 @@ const handleExistingToken = async (tokenSet, props, credentialsPath) => {
|
|
|
67
64
|
return { apiKey: tokenSet.access_token, apiClient };
|
|
68
65
|
}
|
|
69
66
|
// Either access_token is missing or its expired. Refresh the token
|
|
70
|
-
log.debug(tokenSet.
|
|
67
|
+
log.debug(tokenSet.expires_at < Date.now()
|
|
71
68
|
? 'Token is expired, attempting refresh'
|
|
72
69
|
: 'Token is missing access_token, attempting refresh');
|
|
73
70
|
if (!tokenSet.refresh_token) {
|
|
@@ -78,17 +75,16 @@ const handleExistingToken = async (tokenSet, props, credentialsPath) => {
|
|
|
78
75
|
const refreshedTokenSet = await refreshToken({
|
|
79
76
|
oauthHost: props.oauthHost,
|
|
80
77
|
clientId: props.clientId,
|
|
78
|
+
allowUnsafeTls: props.allowUnsafeTls,
|
|
81
79
|
}, tokenSet);
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
const apiKey = refreshedTokenSet.access_token;
|
|
80
|
+
// Extend the token set with expires_at
|
|
81
|
+
const extendedTokenSet = extendTokenSet(refreshedTokenSet);
|
|
82
|
+
const apiKey = extendedTokenSet.access_token;
|
|
87
83
|
const apiClient = getApiClient({
|
|
88
84
|
apiKey,
|
|
89
85
|
apiHost: props.apiHost,
|
|
90
86
|
});
|
|
91
|
-
await preserveCredentials(credentialsPath,
|
|
87
|
+
await preserveCredentials(credentialsPath, extendedTokenSet, apiClient);
|
|
92
88
|
log.debug('Token refresh successful');
|
|
93
89
|
return { apiKey, apiClient };
|
|
94
90
|
}
|
|
@@ -121,8 +117,7 @@ export const ensureAuth = async (props) => {
|
|
|
121
117
|
try {
|
|
122
118
|
const contents = readFileSync(credentialsPath, 'utf8');
|
|
123
119
|
log.debug('Credentials MD5 hash: %s', md5hash(contents));
|
|
124
|
-
const
|
|
125
|
-
const tokenSet = new TokenSet(tokenSetContents);
|
|
120
|
+
const tokenSet = JSON.parse(contents);
|
|
126
121
|
// Try to use existing token or refresh it
|
|
127
122
|
const result = await handleExistingToken(tokenSet, props, credentialsPath);
|
|
128
123
|
if (result) {
|
package/commands/auth.test.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import axios from 'axios';
|
|
2
2
|
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync, } from 'node:fs';
|
|
3
|
-
import { TokenSet } from 'openid-client';
|
|
4
3
|
import { join } from 'path';
|
|
5
4
|
import { afterAll, beforeAll, beforeEach, describe, expect, vi } from 'vitest';
|
|
6
5
|
import * as authModule from '../auth';
|
|
@@ -29,6 +28,7 @@ describe('auth', () => {
|
|
|
29
28
|
configDir,
|
|
30
29
|
forceAuth: true,
|
|
31
30
|
oauthHost: `http://localhost:${oauthServer.address().port}`,
|
|
31
|
+
allowUnsafeTls: true,
|
|
32
32
|
});
|
|
33
33
|
const credentials = JSON.parse(readFileSync(`${configDir}/credentials.json`, 'utf-8'));
|
|
34
34
|
expect(credentials.access_token).toEqual(expect.any(String));
|
|
@@ -68,20 +68,21 @@ describe('ensureAuth', () => {
|
|
|
68
68
|
apiHost: `http://localhost:${server.address().port}`,
|
|
69
69
|
help: false,
|
|
70
70
|
apiClient: mockApiClient,
|
|
71
|
+
allowUnsafeTls: true,
|
|
71
72
|
});
|
|
72
73
|
test('should start new auth flow when refresh token fails', async ({ runMockServer, }) => {
|
|
73
74
|
refreshTokenSpy.mockImplementationOnce(() => Promise.reject(new Error('AUTH_REFRESH_FAILED')));
|
|
74
|
-
authSpy.mockImplementationOnce(() => Promise.resolve(
|
|
75
|
+
authSpy.mockImplementationOnce(() => Promise.resolve({
|
|
75
76
|
access_token: 'new-auth-token',
|
|
76
77
|
refresh_token: 'new-refresh-token',
|
|
77
78
|
expires_at: Math.floor(Date.now() / 1000) + 3600,
|
|
78
|
-
}))
|
|
79
|
+
}));
|
|
79
80
|
const server = await runMockServer('main');
|
|
80
|
-
const expiredTokenSet =
|
|
81
|
+
const expiredTokenSet = {
|
|
81
82
|
access_token: 'expired-token',
|
|
82
83
|
refresh_token: 'refresh-token',
|
|
83
|
-
expires_at:
|
|
84
|
-
}
|
|
84
|
+
expires_at: Date.now() - 3600 * 1000,
|
|
85
|
+
};
|
|
85
86
|
writeFileSync(join(configDir, 'credentials.json'), JSON.stringify(expiredTokenSet), { mode: 0o700 });
|
|
86
87
|
const props = setupTestProps(server);
|
|
87
88
|
await ensureAuth(props);
|
|
@@ -114,15 +115,15 @@ describe('ensureAuth', () => {
|
|
|
114
115
|
});
|
|
115
116
|
test('should try refresh when token is missing access_token but has refresh_token', async ({ runMockServer, }) => {
|
|
116
117
|
const server = await runMockServer('main');
|
|
117
|
-
const tokenWithoutAccess =
|
|
118
|
+
const tokenWithoutAccess = {
|
|
118
119
|
refresh_token: 'refresh-token',
|
|
119
|
-
}
|
|
120
|
+
};
|
|
120
121
|
writeFileSync(join(configDir, 'credentials.json'), JSON.stringify(tokenWithoutAccess), { mode: 0o700 });
|
|
121
|
-
refreshTokenSpy.mockImplementationOnce(() => Promise.resolve(
|
|
122
|
+
refreshTokenSpy.mockImplementationOnce(() => Promise.resolve({
|
|
122
123
|
access_token: 'refreshed-token',
|
|
123
124
|
refresh_token: 'new-refresh-token',
|
|
124
125
|
expires_at: Math.floor(Date.now() / 1000) + 3600,
|
|
125
|
-
}))
|
|
126
|
+
}));
|
|
126
127
|
const props = setupTestProps(server);
|
|
127
128
|
await ensureAuth(props);
|
|
128
129
|
expect(refreshTokenSpy).toHaveBeenCalledTimes(1);
|
|
@@ -131,11 +132,11 @@ describe('ensureAuth', () => {
|
|
|
131
132
|
});
|
|
132
133
|
test('should use existing valid token', async ({ runMockServer }) => {
|
|
133
134
|
const server = await runMockServer('main');
|
|
134
|
-
const validTokenSet =
|
|
135
|
+
const validTokenSet = {
|
|
135
136
|
access_token: 'valid-token',
|
|
136
137
|
refresh_token: 'refresh-token',
|
|
137
|
-
expires_at:
|
|
138
|
-
}
|
|
138
|
+
expires_at: Date.now() + 3600 * 1000, // 1 hour from now
|
|
139
|
+
};
|
|
139
140
|
writeFileSync(join(configDir, 'credentials.json'), JSON.stringify(validTokenSet), { mode: 0o700 });
|
|
140
141
|
const props = setupTestProps(server);
|
|
141
142
|
await ensureAuth(props);
|
|
@@ -144,17 +145,17 @@ describe('ensureAuth', () => {
|
|
|
144
145
|
expect(props.apiKey).toBe('valid-token');
|
|
145
146
|
});
|
|
146
147
|
test('should successfully refresh expired token', async ({ runMockServer, }) => {
|
|
147
|
-
refreshTokenSpy.mockImplementationOnce(() => Promise.resolve(
|
|
148
|
+
refreshTokenSpy.mockImplementationOnce(() => Promise.resolve({
|
|
148
149
|
access_token: 'new-token',
|
|
149
150
|
refresh_token: 'new-refresh-token',
|
|
150
151
|
expires_at: Math.floor(Date.now() / 1000) + 3600,
|
|
151
|
-
}))
|
|
152
|
+
}));
|
|
152
153
|
const server = await runMockServer('main');
|
|
153
|
-
const expiredTokenSet =
|
|
154
|
+
const expiredTokenSet = {
|
|
154
155
|
access_token: 'expired-token',
|
|
155
156
|
refresh_token: 'refresh-token',
|
|
156
|
-
expires_at:
|
|
157
|
-
}
|
|
157
|
+
expires_at: Date.now() - 3600 * 1000, // expired 1 hour ago
|
|
158
|
+
};
|
|
158
159
|
writeFileSync(join(configDir, 'credentials.json'), JSON.stringify(expiredTokenSet), { mode: 0o700 });
|
|
159
160
|
const props = setupTestProps(server);
|
|
160
161
|
await ensureAuth(props);
|
package/commands/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import * as roles from './roles.js';
|
|
|
10
10
|
import * as operations from './operations.js';
|
|
11
11
|
import * as cs from './connection_string.js';
|
|
12
12
|
import * as setContext from './set_context.js';
|
|
13
|
+
import * as init from './init.js';
|
|
13
14
|
export default [
|
|
14
15
|
auth,
|
|
15
16
|
users,
|
|
@@ -23,4 +24,5 @@ export default [
|
|
|
23
24
|
operations,
|
|
24
25
|
cs,
|
|
25
26
|
setContext,
|
|
27
|
+
init,
|
|
26
28
|
];
|
package/commands/init.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import { log } from '../log.js';
|
|
3
|
+
import { sendError } from '../analytics.js';
|
|
4
|
+
export const command = 'init';
|
|
5
|
+
export const describe = 'Initialize a new Neon project using your AI coding assistant';
|
|
6
|
+
export const builder = (yargs) => yargs
|
|
7
|
+
.option('context-file', {
|
|
8
|
+
hidden: true,
|
|
9
|
+
})
|
|
10
|
+
.strict(false);
|
|
11
|
+
export const handler = async (args) => {
|
|
12
|
+
const passThruArgs = args['--'] || [];
|
|
13
|
+
await runNeonInit(passThruArgs);
|
|
14
|
+
};
|
|
15
|
+
const runNeonInit = async (args) => {
|
|
16
|
+
try {
|
|
17
|
+
await execa('npx', ['neon-init', ...args], {
|
|
18
|
+
stdio: 'inherit',
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
// Check if it's an ENOENT error (command not found)
|
|
23
|
+
if (error?.code === 'ENOENT') {
|
|
24
|
+
log.error('npx is not available in the PATH');
|
|
25
|
+
sendError(error, 'NPX_NOT_FOUND');
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
// Check if the process was killed by a signal (user cancelled)
|
|
29
|
+
else if (error?.signal) {
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
// Handle all other errors
|
|
33
|
+
else {
|
|
34
|
+
const exitError = new Error(`failed to run neon-init`);
|
|
35
|
+
sendError(exitError, 'NEON_INIT_FAILED');
|
|
36
|
+
if (typeof error?.exitCode === 'number') {
|
|
37
|
+
process.exit(error.exitCode);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { describe } from 'vitest';
|
|
2
|
+
import { test } from '../test_utils/fixtures';
|
|
3
|
+
describe('init', () => {
|
|
4
|
+
test('init should run neon-init', async ({ testCliCommand }) => {
|
|
5
|
+
await testCliCommand(['init']);
|
|
6
|
+
});
|
|
7
|
+
test('init with an argument', async ({ testCliCommand }) => {
|
|
8
|
+
await testCliCommand(['init', '--', '--debug']);
|
|
9
|
+
});
|
|
10
|
+
});
|
package/index.js
CHANGED
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"url": "git+ssh://git@github.com/neondatabase/neonctl.git"
|
|
6
6
|
},
|
|
7
7
|
"type": "module",
|
|
8
|
-
"version": "2.
|
|
8
|
+
"version": "2.16.0",
|
|
9
9
|
"description": "CLI tool for NeonDB Cloud management",
|
|
10
10
|
"main": "index.js",
|
|
11
11
|
"author": "NeonDB",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"express": "^4.18.2",
|
|
44
44
|
"husky": "^8.0.3",
|
|
45
45
|
"lint-staged": "^13.0.3",
|
|
46
|
-
"oauth2-mock-server": "^
|
|
46
|
+
"oauth2-mock-server": "^8.1.0",
|
|
47
47
|
"pkg": "^5.8.1",
|
|
48
48
|
"prettier": "^3.1.0",
|
|
49
49
|
"rollup": "^3.26.2",
|
|
@@ -62,8 +62,9 @@
|
|
|
62
62
|
"cli-table": "^0.3.11",
|
|
63
63
|
"crypto-random-string": "^5.0.0",
|
|
64
64
|
"diff": "^5.2.0",
|
|
65
|
+
"execa": "^9.6.0",
|
|
65
66
|
"open": "^10.1.0",
|
|
66
|
-
"openid-client": "^
|
|
67
|
+
"openid-client": "^6.8.1",
|
|
67
68
|
"prompts": "2.4.2",
|
|
68
69
|
"validate-npm-package-name": "5.0.1",
|
|
69
70
|
"which": "^3.0.1",
|
|
@@ -4,6 +4,6 @@ export const startOauthServer = async () => {
|
|
|
4
4
|
const server = new OAuth2Server();
|
|
5
5
|
await server.issuer.keys.generate('RS256');
|
|
6
6
|
await server.start(0, 'localhost');
|
|
7
|
-
log.debug('Started OAuth server');
|
|
7
|
+
log.debug('Started OAuth server on port %d', server.address().port);
|
|
8
8
|
return server;
|
|
9
9
|
};
|
package/utils/auth.js
ADDED
package/utils/formats.js
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
|
-
const HAIKU_REGEX = /^[a-
|
|
1
|
+
const HAIKU_REGEX = /^[a-z0-9]+-[a-z0-9]+-[a-z0-9]+$/;
|
|
2
2
|
export const looksLikeBranchId = (branch) => branch.startsWith('br-') && HAIKU_REGEX.test(branch.substring(3));
|
|
3
3
|
const LSN_REGEX = /^[a-fA-F0-9]{1,8}\/[a-fA-F0-9]{1,8}$/;
|
|
4
4
|
export const looksLikeLSN = (lsn) => LSN_REGEX.test(lsn);
|
|
5
|
-
export const looksLikeTimestamp = (timestamp) =>
|
|
5
|
+
export const looksLikeTimestamp = (timestamp) => {
|
|
6
|
+
if (isNaN(Date.parse(timestamp)))
|
|
7
|
+
return false;
|
|
8
|
+
/**
|
|
9
|
+
* @info
|
|
10
|
+
* Check for ISO 8601/RFC 3339 format patterns
|
|
11
|
+
* Must contain 'T' separator and end with 'Z' or timezone offset
|
|
12
|
+
*
|
|
13
|
+
* `Date.parse` aggressive parsing will attempt a date out of any string.
|
|
14
|
+
* so if a branch name has a number, `Date.parse` will return a valid timestamp.
|
|
15
|
+
*/
|
|
16
|
+
const iso8601Regex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?([+-]\d{2}:\d{2}|Z)$/;
|
|
17
|
+
return iso8601Regex.test(timestamp);
|
|
18
|
+
};
|