@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 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.960e382 linux-x64 node-v24.21.0
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
@@ -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 CONFIGURATION = {
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 ?? CONFIGURATION.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
- ...(config?.apiGateway && { apiGateway: config.apiGateway }),
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: CONFIGURATION.userPoolId,
110
- ClientId: CONFIGURATION.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
- const user = result;
127
- const config = await setAuthContext(user, params);
128
- resolve(config);
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?.apiGateway ?? CONFIGURATION.apiGateway;
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
- return await response.json().then((data) => {
185
- return { vibeAccessToken: data.accessToken, vibeRefreshToken: data.refreshToken };
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/`;
@@ -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' }]);
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "3.5.5-alpha.960e382",
2
+ "version": "3.5.5-alpha.a4cf3d0",
3
3
  "commands": {
4
4
  "app:create": {
5
5
  "id": "app:create",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrail/cli",
3
- "version": "3.5.5-alpha.960e382",
3
+ "version": "3.5.5-alpha.a4cf3d0",
4
4
  "description": "VibeIQ's Contrail Platform CLI.",
5
5
  "bin": {
6
6
  "contrail-cli": "./bin/run",