@contrail/cli 3.5.5-alpha.960e382 → 3.5.5-alpha.a4cf3d0
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/README.md +1 -1
- package/dist/auth/login.js +34 -16
- package/dist/auth/login.spec.js +82 -0
- package/dist/commands/feature-flags/import.d.ts +4 -0
- package/dist/commands/feature-flags/import.js +10 -1
- package/dist/commands/feature-flags/import.spec.js +20 -0
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ $ npm install -g @contrail/cli
|
|
|
17
17
|
$ contrail COMMAND
|
|
18
18
|
running command...
|
|
19
19
|
$ contrail (--version)
|
|
20
|
-
@contrail/cli/3.5.5-alpha.
|
|
20
|
+
@contrail/cli/3.5.5-alpha.a4cf3d0 linux-x64 node-v24.21.0
|
|
21
21
|
$ contrail --help [COMMAND]
|
|
22
22
|
USAGE
|
|
23
23
|
$ contrail COMMAND
|
package/dist/auth/login.js
CHANGED
|
@@ -9,11 +9,7 @@ const config_1 = require("../helpers/config/config");
|
|
|
9
9
|
const logger_1 = tslib_1.__importDefault(require("../logger"));
|
|
10
10
|
const path_1 = tslib_1.__importDefault(require("path"));
|
|
11
11
|
const fs_1 = tslib_1.__importDefault(require("fs"));
|
|
12
|
-
const
|
|
13
|
-
userPoolId: 'us-east-1_qIaNZdUfm',
|
|
14
|
-
clientId: process.env.CONTRAIL_COGNITO_CLIENT_ID || '7knkt34gfacbgd0ghnuspefdfg',
|
|
15
|
-
apiGateway: 'https://api.vibeiq.com/prod/api',
|
|
16
|
-
};
|
|
12
|
+
const DEFAULT_API_GATEWAY = config_1.ENVIRONMENTS.prod;
|
|
17
13
|
async function authUser() {
|
|
18
14
|
logger_1.default.debug('authorizing user...');
|
|
19
15
|
const config = await (0, config_1.getConfig)();
|
|
@@ -66,7 +62,7 @@ async function getOrgSlug(config) {
|
|
|
66
62
|
return orgSlug;
|
|
67
63
|
}
|
|
68
64
|
async function authenticateWithApiKey(config, apiKey, orgSlug) {
|
|
69
|
-
const apiGateway = config?.apiGateway ??
|
|
65
|
+
const apiGateway = config?.apiGateway ?? DEFAULT_API_GATEWAY;
|
|
70
66
|
sdk_1.Auth.config = {
|
|
71
67
|
apiUserToken: apiKey,
|
|
72
68
|
apiRefreshToken: null,
|
|
@@ -78,6 +74,10 @@ async function authenticateWithApiKey(config, apiKey, orgSlug) {
|
|
|
78
74
|
return returnAndStoreConfig(sdk_1.Auth.config);
|
|
79
75
|
}
|
|
80
76
|
async function authenticateWithUsernamePassword(config) {
|
|
77
|
+
const apiGateway = config?.apiGateway ?? DEFAULT_API_GATEWAY;
|
|
78
|
+
const cognitoConfig = await fetchCognitoConfig(apiGateway);
|
|
79
|
+
const environment = describeEnvironment(apiGateway);
|
|
80
|
+
logger_1.default.info(`Signing in to ${environment} — use your ${environment} VibeIQ credentials.`);
|
|
81
81
|
const questions = [];
|
|
82
82
|
if (!process.env.CONTRAIL_CLI_EMAIL) {
|
|
83
83
|
questions.push({
|
|
@@ -100,14 +100,14 @@ async function authenticateWithUsernamePassword(config) {
|
|
|
100
100
|
email: process.env.CONTRAIL_CLI_EMAIL || answers.email,
|
|
101
101
|
password: process.env.CONTRAIL_CLI_PASSWORD || answers.password,
|
|
102
102
|
orgSlug: await getOrgSlug(config),
|
|
103
|
-
|
|
103
|
+
apiGateway,
|
|
104
104
|
};
|
|
105
105
|
if (!params.email || !params.password) {
|
|
106
106
|
throw new Error('Email and Password required');
|
|
107
107
|
}
|
|
108
108
|
const poolData = {
|
|
109
|
-
UserPoolId:
|
|
110
|
-
ClientId:
|
|
109
|
+
UserPoolId: cognitoConfig.userPoolId,
|
|
110
|
+
ClientId: cognitoConfig.clientId,
|
|
111
111
|
};
|
|
112
112
|
const userPool = new amazon_cognito_identity_js_1.CognitoUserPool(poolData);
|
|
113
113
|
const authData = {
|
|
@@ -123,9 +123,12 @@ async function authenticateWithUsernamePassword(config) {
|
|
|
123
123
|
return new Promise((resolve, reject) => {
|
|
124
124
|
cognitoUser.authenticateUser(authDetails, {
|
|
125
125
|
onSuccess: async (result) => {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
126
|
+
try {
|
|
127
|
+
resolve(await setAuthContext(result, params));
|
|
128
|
+
}
|
|
129
|
+
catch (e) {
|
|
130
|
+
reject(e);
|
|
131
|
+
}
|
|
129
132
|
},
|
|
130
133
|
onFailure: (err) => {
|
|
131
134
|
reject(err);
|
|
@@ -134,7 +137,7 @@ async function authenticateWithUsernamePassword(config) {
|
|
|
134
137
|
});
|
|
135
138
|
}
|
|
136
139
|
async function setAuthContext(user, params) {
|
|
137
|
-
const apiGateway = params
|
|
140
|
+
const apiGateway = params.apiGateway;
|
|
138
141
|
const { vibeAccessToken, vibeRefreshToken } = await getVibeAccessTokenFromCognitoAccessToken(user.idToken.jwtToken, apiGateway);
|
|
139
142
|
// Set Auth Config
|
|
140
143
|
sdk_1.Auth.config = {
|
|
@@ -171,6 +174,19 @@ async function refreshCurrentUserOrg() {
|
|
|
171
174
|
// await checkTelemetry(current_Version);
|
|
172
175
|
return returnAndStoreConfig(sdk_1.Auth.config);
|
|
173
176
|
}
|
|
177
|
+
async function fetchCognitoConfig(apiGateway) {
|
|
178
|
+
const response = await fetch(`${apiGateway}/auth/cognito/config`, {
|
|
179
|
+
headers: { Accept: 'application/json' },
|
|
180
|
+
});
|
|
181
|
+
if (!response.ok) {
|
|
182
|
+
throw new Error(`${describeEnvironment(apiGateway)} did not return its sign-in configuration (${response.status}).`);
|
|
183
|
+
}
|
|
184
|
+
return await response.json();
|
|
185
|
+
}
|
|
186
|
+
function describeEnvironment(apiGateway) {
|
|
187
|
+
return (Object.keys(config_1.ENVIRONMENTS).find((name) => config_1.ENVIRONMENTS[name] === apiGateway) ??
|
|
188
|
+
new URL(apiGateway).host);
|
|
189
|
+
}
|
|
174
190
|
async function getVibeAccessTokenFromCognitoAccessToken(cognitoAccessToken, apiGateway) {
|
|
175
191
|
const url = `${apiGateway}/auth/cognito/`;
|
|
176
192
|
const response = await fetch(url, {
|
|
@@ -181,9 +197,11 @@ async function getVibeAccessTokenFromCognitoAccessToken(cognitoAccessToken, apiG
|
|
|
181
197
|
},
|
|
182
198
|
body: JSON.stringify({ accessToken: cognitoAccessToken }),
|
|
183
199
|
});
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
200
|
+
if (!response.ok) {
|
|
201
|
+
throw new Error(`${describeEnvironment(apiGateway)} rejected the sign-in (${response.status}).`);
|
|
202
|
+
}
|
|
203
|
+
const data = await response.json();
|
|
204
|
+
return { vibeAccessToken: data.accessToken, vibeRefreshToken: data.refreshToken };
|
|
187
205
|
}
|
|
188
206
|
async function requestRefreshedAccessToken(vibeRefreshToken, apiGateway) {
|
|
189
207
|
const url = `${apiGateway}/auth/jwt_refresh/`;
|
package/dist/auth/login.spec.js
CHANGED
|
@@ -1,6 +1,88 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const amazon_cognito_identity_js_1 = require("amazon-cognito-identity-js");
|
|
4
|
+
const sdk_1 = require("@contrail/sdk");
|
|
5
|
+
const config_1 = require("../helpers/config/config");
|
|
3
6
|
const login_1 = require("./login");
|
|
7
|
+
jest.mock('amazon-cognito-identity-js', () => ({
|
|
8
|
+
CognitoUserPool: jest.fn(),
|
|
9
|
+
AuthenticationDetails: jest.fn(),
|
|
10
|
+
CognitoUser: jest.fn().mockImplementation(() => ({
|
|
11
|
+
authenticateUser: (_details, callbacks) => callbacks.onSuccess({ idToken: { jwtToken: 'cognito-id-token' } }),
|
|
12
|
+
})),
|
|
13
|
+
}));
|
|
14
|
+
jest.mock('@contrail/sdk', () => ({
|
|
15
|
+
Auth: { config: {} },
|
|
16
|
+
getCurrentUser: jest.fn(),
|
|
17
|
+
setConfig: jest.fn(),
|
|
18
|
+
}));
|
|
19
|
+
jest.mock('../helpers/config/config', () => ({
|
|
20
|
+
...jest.requireActual('../helpers/config/config'),
|
|
21
|
+
getConfig: jest.fn(),
|
|
22
|
+
storeConfig: jest.fn(),
|
|
23
|
+
}));
|
|
24
|
+
jest.mock('../logger', () => ({
|
|
25
|
+
__esModule: true,
|
|
26
|
+
default: { debug: jest.fn(), info: jest.fn() },
|
|
27
|
+
}));
|
|
28
|
+
const DEV_GATEWAY = 'https://api.dev.vibeiq.com/dev/api';
|
|
29
|
+
const DEV_COGNITO_CONFIG = {
|
|
30
|
+
region: 'us-east-1',
|
|
31
|
+
userPoolId: 'us-east-1_e2cA0nJWu',
|
|
32
|
+
clientId: '209s9c62o165cqab0fu5ihjtu9',
|
|
33
|
+
};
|
|
34
|
+
function jsonResponse(status, body = {}) {
|
|
35
|
+
return new Response(JSON.stringify(body), { status });
|
|
36
|
+
}
|
|
37
|
+
describe('authUser with email and password', () => {
|
|
38
|
+
const originalFetch = global.fetch;
|
|
39
|
+
const originalEmail = process.env.CONTRAIL_CLI_EMAIL;
|
|
40
|
+
const originalPassword = process.env.CONTRAIL_CLI_PASSWORD;
|
|
41
|
+
let fetchMock;
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
jest.clearAllMocks();
|
|
44
|
+
process.env.CONTRAIL_CLI_EMAIL = 'dev@vibeiq.com';
|
|
45
|
+
process.env.CONTRAIL_CLI_PASSWORD = 'secret';
|
|
46
|
+
config_1.getConfig.mockResolvedValue({ apiGateway: DEV_GATEWAY, orgSlug: 'adam-zone-1' });
|
|
47
|
+
sdk_1.getCurrentUser.mockResolvedValue({ id: 'user-1' });
|
|
48
|
+
fetchMock = jest.fn();
|
|
49
|
+
global.fetch = fetchMock;
|
|
50
|
+
});
|
|
51
|
+
afterEach(() => {
|
|
52
|
+
global.fetch = originalFetch;
|
|
53
|
+
process.env.CONTRAIL_CLI_EMAIL = originalEmail;
|
|
54
|
+
process.env.CONTRAIL_CLI_PASSWORD = originalPassword;
|
|
55
|
+
});
|
|
56
|
+
it('should sign in through the pool the configured environment reports', async () => {
|
|
57
|
+
fetchMock
|
|
58
|
+
.mockResolvedValueOnce(jsonResponse(200, DEV_COGNITO_CONFIG))
|
|
59
|
+
.mockResolvedValueOnce(jsonResponse(200, { accessToken: 'vibe-access', refreshToken: 'vibe-refresh' }));
|
|
60
|
+
const context = await (0, login_1.authUser)();
|
|
61
|
+
expect(fetchMock.mock.calls[0][0]).toBe(`${DEV_GATEWAY}/auth/cognito/config`);
|
|
62
|
+
expect(amazon_cognito_identity_js_1.CognitoUserPool).toHaveBeenCalledWith({
|
|
63
|
+
UserPoolId: DEV_COGNITO_CONFIG.userPoolId,
|
|
64
|
+
ClientId: DEV_COGNITO_CONFIG.clientId,
|
|
65
|
+
});
|
|
66
|
+
expect(context).toEqual({
|
|
67
|
+
apiUserToken: 'vibe-access',
|
|
68
|
+
apiRefreshToken: 'vibe-refresh',
|
|
69
|
+
apiGateway: DEV_GATEWAY,
|
|
70
|
+
orgSlug: 'adam-zone-1',
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
it('should fail without saving tokens when the environment has no sign-in configuration', async () => {
|
|
74
|
+
fetchMock.mockResolvedValueOnce(jsonResponse(404));
|
|
75
|
+
await expect((0, login_1.authUser)()).rejects.toThrow('dev did not return its sign-in configuration (404).');
|
|
76
|
+
expect(config_1.storeConfig).not.toHaveBeenCalled();
|
|
77
|
+
});
|
|
78
|
+
it('should fail without saving tokens when the environment rejects the sign-in', async () => {
|
|
79
|
+
fetchMock
|
|
80
|
+
.mockResolvedValueOnce(jsonResponse(200, DEV_COGNITO_CONFIG))
|
|
81
|
+
.mockResolvedValueOnce(jsonResponse(401, { message: 'Unauthorized' }));
|
|
82
|
+
await expect((0, login_1.authUser)()).rejects.toThrow('dev rejected the sign-in (401).');
|
|
83
|
+
expect(config_1.storeConfig).not.toHaveBeenCalled();
|
|
84
|
+
});
|
|
85
|
+
});
|
|
4
86
|
describe('getVersionFromPackageJson', () => {
|
|
5
87
|
it('should return the version value from package.json', () => {
|
|
6
88
|
const version = (0, login_1.getVersionFromPackageJson)();
|
|
@@ -34,5 +34,9 @@ export declare function uploadFeatureFlagsIfPermitted(dir: string): Promise<void
|
|
|
34
34
|
*
|
|
35
35
|
* Failures are collected and rethrown so an import never reports success while the org is
|
|
36
36
|
* missing flags.
|
|
37
|
+
*
|
|
38
|
+
* A file that lists no flags at all is skipped rather than applied: reconciling against an
|
|
39
|
+
* empty list would disable every flag in the target org, which is almost never what a copy
|
|
40
|
+
* is meant to do. Flags that should be off in the target have to be disabled by hand.
|
|
37
41
|
*/
|
|
38
42
|
export declare function uploadFeatureFlags(dir: string): Promise<void>;
|
|
@@ -107,6 +107,10 @@ exports.uploadFeatureFlagsIfPermitted = uploadFeatureFlagsIfPermitted;
|
|
|
107
107
|
*
|
|
108
108
|
* Failures are collected and rethrown so an import never reports success while the org is
|
|
109
109
|
* missing flags.
|
|
110
|
+
*
|
|
111
|
+
* A file that lists no flags at all is skipped rather than applied: reconciling against an
|
|
112
|
+
* empty list would disable every flag in the target org, which is almost never what a copy
|
|
113
|
+
* is meant to do. Flags that should be off in the target have to be disabled by hand.
|
|
110
114
|
*/
|
|
111
115
|
async function uploadFeatureFlags(dir) {
|
|
112
116
|
const filePath = path.join(dir, export_1.FEATURE_FLAGS_FILE);
|
|
@@ -114,11 +118,16 @@ async function uploadFeatureFlags(dir) {
|
|
|
114
118
|
logger_1.default.info(`${chalk_1.default.yellow.bold('FEATURE FLAGS')} No feature flags found in ${dir}. Skipping.`);
|
|
115
119
|
return;
|
|
116
120
|
}
|
|
117
|
-
const data = (0, file_helpers_1.loadData)(filePath);
|
|
121
|
+
const data = (0, file_helpers_1.loadData)(filePath) ?? [];
|
|
118
122
|
if (!Array.isArray(data)) {
|
|
119
123
|
throw new Error(`${filePath} must contain a YAML list of feature flags`);
|
|
120
124
|
}
|
|
121
125
|
const featureNamesToEnable = new Set(data.map(toFeatureName).filter(Boolean));
|
|
126
|
+
if (featureNamesToEnable.size === 0) {
|
|
127
|
+
logger_1.default.info(`${chalk_1.default.yellow.bold('FEATURE FLAGS')} ${filePath} lists no feature flags. Skipping, so ` +
|
|
128
|
+
'the flags currently enabled in this org are left as they are.');
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
122
131
|
const existingFlags = (await (0, feature_flags_1.getFeatureFlags)()) ?? [];
|
|
123
132
|
const enabledFeatureNames = new Set(existingFlags.map((flag) => flag?.featureName));
|
|
124
133
|
const toCreate = [...featureNamesToEnable].filter((name) => !enabledFeatureNames.has(name));
|
|
@@ -58,6 +58,26 @@ describe('uploadFeatureFlags', () => {
|
|
|
58
58
|
expect(mocked(feature_flags_1.getFeatureFlags)).not.toHaveBeenCalled();
|
|
59
59
|
expect(mocked(feature_flags_1.createFeatureFlag)).not.toHaveBeenCalled();
|
|
60
60
|
});
|
|
61
|
+
it('skips an export that lists no flags instead of disabling everything in the target org', async () => {
|
|
62
|
+
mocked(file_helpers_1.loadData).mockReturnValue([]);
|
|
63
|
+
mocked(feature_flags_1.getFeatureFlags).mockResolvedValue([
|
|
64
|
+
{ id: '1', featureName: 'SHOWROOM' },
|
|
65
|
+
{ id: '2', featureName: 'DEV_TABS' },
|
|
66
|
+
]);
|
|
67
|
+
const infoSpy = jest.spyOn(logger_1.default, 'info').mockImplementation(() => undefined);
|
|
68
|
+
await (0, import_1.uploadFeatureFlags)('export-dir');
|
|
69
|
+
expect(mocked(feature_flags_1.deleteFeatureFlag)).not.toHaveBeenCalled();
|
|
70
|
+
expect(mocked(feature_flags_1.createFeatureFlag)).not.toHaveBeenCalled();
|
|
71
|
+
expect(infoSpy).toHaveBeenCalledWith(expect.stringContaining('lists no feature flags'));
|
|
72
|
+
infoSpy.mockRestore();
|
|
73
|
+
});
|
|
74
|
+
it('treats a blank feature-flags.yml the same as an empty list', async () => {
|
|
75
|
+
mocked(file_helpers_1.loadData).mockReturnValue(undefined);
|
|
76
|
+
mocked(feature_flags_1.getFeatureFlags).mockResolvedValue([{ id: '1', featureName: 'SHOWROOM' }]);
|
|
77
|
+
await (0, import_1.uploadFeatureFlags)('export-dir');
|
|
78
|
+
expect(mocked(feature_flags_1.getFeatureFlags)).not.toHaveBeenCalled();
|
|
79
|
+
expect(mocked(feature_flags_1.deleteFeatureFlag)).not.toHaveBeenCalled();
|
|
80
|
+
});
|
|
61
81
|
it('enables every flag from the export that the target org is missing', async () => {
|
|
62
82
|
mocked(file_helpers_1.loadData).mockReturnValue([{ featureName: 'SHOWROOM' }, { featureName: 'PROMOTE' }]);
|
|
63
83
|
mocked(feature_flags_1.getFeatureFlags).mockResolvedValue([{ id: '1', featureName: 'SHOWROOM' }]);
|
package/oclif.manifest.json
CHANGED