@contentful/app-scripts 1.8.2 → 1.10.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/bin/app-scripts CHANGED
@@ -29,6 +29,7 @@ async function runCommand(command, options) {
29
29
  .option('--token [accessToken]', 'Your content management access token')
30
30
  .option('--comment [comment]', 'Optional comment for the created bundle')
31
31
  .option('--skip-activation', 'A Boolean flag to skip automatic activation')
32
+ .option('--host [host]', 'Contentful domain to use')
32
33
  .action(async (options) => {
33
34
  await runCommand(upload, options);
34
35
  });
@@ -40,6 +41,7 @@ async function runCommand(command, options) {
40
41
  .option('--organization-id [orgId]', 'The id of your organization')
41
42
  .option('--definition-id [defId]', 'The id of your apps definition')
42
43
  .option('--token [accessToken]', 'Your content management access token')
44
+ .option('--host [host]', 'Contentful domain to use')
43
45
  .action(async (options) => {
44
46
  await runCommand(activate, options);
45
47
  });
@@ -59,14 +61,14 @@ async function runCommand(command, options) {
59
61
  .option('--definition-id [defId]', 'The id of your apps definition')
60
62
  .option('--token [accessToken]', 'Your content management access token')
61
63
  .option('--keep [keepAmount]', 'The amount of bundles that should remain')
62
- .option('--host [host]', 'Contentful CMA-endpoint to use')
64
+ .option('--host [host]', 'Contentful domain to use')
63
65
  .action(async (options) => {
64
66
  await runCommand(cleanup, options);
65
67
  });
66
68
 
67
69
  program.hook('preAction', (thisCommand) => {
68
- track({ command: thisCommand.args[0], ci: `${thisCommand._optionValues.ci}` })
69
- })
70
+ track({ command: thisCommand.args[0], ci: `${thisCommand._optionValues.ci}` });
71
+ });
70
72
 
71
73
  await program.parseAsync(process.argv);
72
74
  })().catch((e) => {
@@ -3,9 +3,9 @@ const chalk = require('chalk');
3
3
  const { throwError } = require('../utils');
4
4
  const { createClient } = require('contentful-management');
5
5
 
6
- async function activateBundle({ accessToken, organization, definition, bundleId }) {
6
+ async function activateBundle({ accessToken, organization, definition, bundleId, host }) {
7
7
  const activationSpinner = ora('Activating your bundle').start();
8
- const plainClient = createClient({ accessToken }, { type: 'plain' });
8
+ const plainClient = createClient({ accessToken, host }, { type: 'plain' });
9
9
  const defaultLocations = [{ location: 'dialog' }];
10
10
 
11
11
  const currentDefinition = await plainClient.appDefinition.get({
@@ -10,7 +10,8 @@ const mockedSettings = {
10
10
  };
11
11
 
12
12
  describe('activate-bundle', () => {
13
- let activateBundle, clientMock, updateStub;
13
+ // eslint-disable-next-line no-unused-vars
14
+ let activateBundle, clientMock, updateStub, createClientArgs;
14
15
  beforeEach(() => {
15
16
  stub(console, 'log');
16
17
  });
@@ -23,7 +24,7 @@ describe('activate-bundle', () => {
23
24
  const definitionMock = {
24
25
  src: 'src',
25
26
  bundle: undefined,
26
- locations: []
27
+ locations: [],
27
28
  };
28
29
 
29
30
  beforeEach(() => {
@@ -37,7 +38,8 @@ describe('activate-bundle', () => {
37
38
 
38
39
  ({ activateBundle } = proxyquire('./activate-bundle', {
39
40
  'contentful-management': {
40
- createClient: () => {
41
+ createClient: (...args) => {
42
+ createClientArgs = args;
41
43
  return clientMock;
42
44
  },
43
45
  },
@@ -60,4 +62,8 @@ describe('activate-bundle', () => {
60
62
  await activateBundle(mockedSettings);
61
63
  assert.strictEqual(throwErrorStub.called, true);
62
64
  });
65
+ it('supports custom defined host domain', async () => {
66
+ await activateBundle({ ...mockedSettings, host: 'jane.doe.com' });
67
+ assert.strictEqual(createClientArgs[0].host, 'jane.doe.com');
68
+ });
63
69
  });
@@ -4,7 +4,7 @@ const inquirer = require('inquirer');
4
4
  const { getAppInfo } = require('../get-app-info');
5
5
 
6
6
  async function buildBundleActivateSettings(options) {
7
- let bundleId = options.bundleId;
7
+ let { bundleId, host } = options;
8
8
  if (!bundleId) {
9
9
  const prompts = await inquirer.prompt([
10
10
  {
@@ -22,6 +22,7 @@ async function buildBundleActivateSettings(options) {
22
22
  return {
23
23
  ...appInfo,
24
24
  bundleId,
25
+ host,
25
26
  };
26
27
  }
27
28
 
@@ -20,6 +20,7 @@ async function getActivateSettingsArgs(options) {
20
20
  ...appInfo,
21
21
  bundleId: options.bundleId,
22
22
  comment: options.comment,
23
+ host: options.host,
23
24
  };
24
25
  } catch (err) {
25
26
  console.log(`
@@ -7,14 +7,15 @@ const { getActionsManifest } = require('../utils');
7
7
  async function buildAppUploadSettings(options) {
8
8
  const actionsManifest = getActionsManifest();
9
9
  const prompts = [];
10
- if (!options.bundleDir) {
10
+ const { bundleDir, comment, skipActivation, host } = options;
11
+ if (!bundleDir) {
11
12
  prompts.push({
12
13
  name: 'bundleDirectory',
13
14
  message: `Bundle directory, if not default:`,
14
15
  default: './build',
15
16
  });
16
17
  }
17
- if (!options.comment) {
18
+ if (!comment) {
18
19
  prompts.push({
19
20
  name: 'comment',
20
21
  message: `Add a comment to the created bundle:`,
@@ -27,9 +28,10 @@ async function buildAppUploadSettings(options) {
27
28
  const appInfo = await getAppInfo(options);
28
29
  // Add app-config & dialog automatically
29
30
  return {
30
- bundleDirectory: options.bundleDir,
31
- skipActivation: !!options.skipActivation,
32
- comment: options.comment,
31
+ bundleDirectory: bundleDir,
32
+ skipActivation: !!skipActivation,
33
+ comment,
34
+ host,
33
35
  actions: actionsManifest,
34
36
  ...appUploadSettings,
35
37
  ...appInfo,
@@ -6,12 +6,12 @@ const { createClient } = require('contentful-management');
6
6
  const { createAppUpload } = require('./create-app-upload');
7
7
 
8
8
  async function createAppBundleFromUpload(settings, appUploadId) {
9
+ const { accessToken, host, userAgentApplication, comment, actions } = settings;
9
10
  const clientSpinner = ora('Verifying your upload...').start();
10
11
  const client = createClient({
11
- accessToken: settings.accessToken,
12
- application: settings.userAgentApplication
13
- ? settings.userAgentApplication
14
- : 'contentful.app-scripts',
12
+ accessToken,
13
+ host,
14
+ application: userAgentApplication ? userAgentApplication : 'contentful.app-scripts',
15
15
  });
16
16
  const organization = await client.getOrganization(settings.organization.value);
17
17
  const appDefinition = await organization.getAppDefinition(settings.definition.value);
@@ -22,8 +22,8 @@ async function createAppBundleFromUpload(settings, appUploadId) {
22
22
  try {
23
23
  appBundle = await appDefinition.createAppBundle({
24
24
  appUploadId,
25
- comment: settings.comment && settings.comment.length > 0 ? settings.comment : undefined,
26
- actions: settings.actions,
25
+ comment: comment && comment.length > 0 ? comment : undefined,
26
+ actions,
27
27
  });
28
28
  } catch (err) {
29
29
  showCreationError('app upload', err.message);
@@ -10,7 +10,8 @@ const mockedSettings = {
10
10
  };
11
11
 
12
12
  describe('createAppBundleFromUpload', () => {
13
- let createAppBundleFromUpload, clientMock;
13
+ // eslint-disable-next-line no-unused-vars
14
+ let createAppBundleFromUpload, clientMock, createClientArgs;
14
15
 
15
16
  beforeEach(() => {
16
17
  stub(console, 'log');
@@ -32,7 +33,8 @@ describe('createAppBundleFromUpload', () => {
32
33
 
33
34
  ({ createAppBundleFromUpload } = proxyquire('./create-app-bundle', {
34
35
  'contentful-management': {
35
- createClient: () => {
36
+ createClient: (...args) => {
37
+ createClientArgs = args;
36
38
  return clientMock;
37
39
  },
38
40
  },
@@ -52,10 +54,14 @@ describe('createAppBundleFromUpload', () => {
52
54
 
53
55
  assert(console.log.calledWith(match(/Creation error:/)));
54
56
  });
57
+ it('supports custom defined host domain creating appbundle from upload', async () => {
58
+ await createAppBundleFromUpload({ ...mockedSettings, host: 'jane.doe.com' });
59
+ assert.strictEqual(createClientArgs[0].host, 'jane.doe.com');
60
+ });
55
61
  });
56
62
 
57
63
  describe('createAppBundleFromSettings', () => {
58
- let createAppBundleFromSettings, clientMock, uploadMock;
64
+ let createAppBundleFromSettings, clientMock, uploadMock, createClientArgs;
59
65
 
60
66
  beforeEach(() => {
61
67
  stub(console, 'log');
@@ -79,7 +85,8 @@ describe('createAppBundleFromSettings', () => {
79
85
 
80
86
  ({ createAppBundleFromSettings } = proxyquire('./create-app-bundle', {
81
87
  'contentful-management': {
82
- createClient: () => {
88
+ createClient: (...args) => {
89
+ createClientArgs = args;
83
90
  return clientMock;
84
91
  },
85
92
  },
@@ -96,4 +103,8 @@ describe('createAppBundleFromSettings', () => {
96
103
  await createAppBundleFromSettings(mockedSettings);
97
104
  assert(console.log.calledWith(match(/Creation error:/)));
98
105
  });
106
+ it('supports custom defined host domain creating appbundle from settings', async () => {
107
+ await createAppBundleFromSettings({ ...mockedSettings, host: 'jane.doe.com' });
108
+ assert.strictEqual(createClientArgs[0].host, 'jane.doe.com');
109
+ });
99
110
  });
@@ -14,16 +14,18 @@ const requiredOptions = {
14
14
  async function getUploadSettingsArgs(options) {
15
15
  const validateSpinner = ora('Validating your input...').start();
16
16
  const actionsManifest = getActionsManifest();
17
+ const { bundleDir, comment, skipActivation, host, userAgentApplication } = options;
17
18
 
18
19
  try {
19
20
  validateArguments(requiredOptions, options, 'upload');
20
21
  const appInfo = await getAppInfo(options);
21
22
  return {
22
23
  ...appInfo,
23
- bundleDirectory: options.bundleDir,
24
- skipActivation: options.skipActivation,
25
- comment: options.comment,
26
- userAgentApplication: options.userAgentApplication,
24
+ bundleDirectory: bundleDir,
25
+ skipActivation,
26
+ comment,
27
+ host,
28
+ userAgentApplication,
27
29
  actions: actionsManifest,
28
30
  };
29
31
  } catch (err) {
package/lib/utils.js CHANGED
@@ -13,6 +13,18 @@ const throwValidationException = (subject, message, details) => {
13
13
  throw new TypeError(message);
14
14
  };
15
15
 
16
+ const isValidNetwork = (address) => {
17
+ const addressRegex =
18
+ /^(?:localhost|(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,6}|(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|(\[(?:[A-Fa-f0-9]{1,4}:){7}[A-Fa-f0-9]{1,4}\]|(?:[A-Fa-f0-9]{1,4}:){7}[A-Fa-f0-9]{1,4}))(?::\d{1,5})?$/;
19
+ return addressRegex.test(address);
20
+ };
21
+
22
+ const stripProtocol = (url) => {
23
+ const protocolRemovedUrl = url.replace(/^https?:\/\//, '');
24
+
25
+ return protocolRemovedUrl.split('/')[0];
26
+ };
27
+
16
28
  const showCreationError = (subject, message) => {
17
29
  console.log(`
18
30
  ${chalk.red('Creation error:')}
@@ -81,7 +93,36 @@ function getActionsManifest() {
81
93
  ----------------------------`);
82
94
  console.log('');
83
95
 
84
- return manifest.actions.map((action) => ({ parameters: [], ...action })); // adding required parameters
96
+ const actions = manifest.actions.map((action) => {
97
+ const allowNetworks = Array.isArray(action.allowNetworks)
98
+ ? action.allowNetworks.map(stripProtocol)
99
+ : [];
100
+
101
+ const hasInvalidNetwork = allowNetworks.find((netWork) => !isValidNetwork(netWork));
102
+ if (hasInvalidNetwork) {
103
+ console.log(
104
+ `${chalk.red(
105
+ 'Error:'
106
+ )} Invalid IP address ${hasInvalidNetwork} found in the allowNetworks array for action "${
107
+ action.name
108
+ }".`
109
+ );
110
+ // eslint-disable-next-line no-process-exit
111
+ process.exit(1);
112
+ }
113
+
114
+ // EntryFile is not used but we do want to strip it from action
115
+ // eslint-disable-next-line no-unused-vars
116
+ const { entryFile: _, ...actionWithoutEntryFile } = action;
117
+
118
+ return {
119
+ parameters: [],
120
+ ...actionWithoutEntryFile,
121
+ allowNetworks,
122
+ };
123
+ });
124
+
125
+ return actions;
85
126
  } catch {
86
127
  console.log(
87
128
  `${chalk.red('Error:')} Invalid JSON in manifest file at ${chalk.bold(
@@ -99,4 +140,6 @@ module.exports = {
99
140
  selectFromList,
100
141
  showCreationError,
101
142
  getActionsManifest,
143
+ isValidNetwork,
144
+ stripProtocol,
102
145
  };
@@ -0,0 +1,198 @@
1
+ const assert = require('assert');
2
+ const { stub } = require('sinon');
3
+ const proxyquire = require('proxyquire');
4
+
5
+ const { isValidNetwork, stripProtocol } = require('./utils');
6
+
7
+ describe('isValidIpAddress', () => {
8
+ it('returns true for a valid IP address', () => {
9
+ const result = isValidNetwork('192.168.0.1');
10
+ assert.strictEqual(result, true);
11
+ });
12
+
13
+ it('returns true for a valid domain', () => {
14
+ const result = isValidNetwork('google.com');
15
+ assert.strictEqual(result, true);
16
+ });
17
+
18
+ it('returns true for a valid ipv6 address', () => {
19
+ const result = isValidNetwork('2001:0db8:85a3:0000:0000:8a2e:0370:7334');
20
+ assert.strictEqual(result, true);
21
+ });
22
+
23
+ it('returns true for a valid domain with port', () => {
24
+ const result = isValidNetwork('google.com:4000');
25
+ assert.strictEqual(result, true);
26
+ });
27
+
28
+ it('returns true for a valid IP address with port', () => {
29
+ const result = isValidNetwork('192.168.0.1:2000');
30
+ assert.strictEqual(result, true);
31
+ });
32
+
33
+ it('returns false for an invalid IP address', () => {
34
+ const result = isValidNetwork('not an ip address');
35
+ assert.strictEqual(result, false);
36
+ });
37
+
38
+ it('returns false for an invalid IP address', () => {
39
+ const result = isValidNetwork('427.0.0.1');
40
+ assert.strictEqual(result, false);
41
+ });
42
+
43
+ it('returns true for an valid ipv6 address with port', () => {
44
+ const result = isValidNetwork('[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:443');
45
+ assert.strictEqual(result, true);
46
+ });
47
+ });
48
+
49
+ describe('removeProtocolFromUrl', () => {
50
+ it('returns the host of a URL without a protocol', () => {
51
+ const result = stripProtocol('http://example.com');
52
+ assert.strictEqual(result, 'example.com');
53
+ });
54
+
55
+ it('returns valid ipv6 address', () => {
56
+ const result = stripProtocol('http://[2001:0db8:85a3:0000:0000:8a2e:0370]:7334');
57
+ assert.strictEqual(result, '[2001:0db8:85a3:0000:0000:8a2e:0370]:7334');
58
+ });
59
+
60
+ it('returns valid domain', () => {
61
+ const result = stripProtocol('example.com');
62
+ assert.strictEqual(result, 'example.com');
63
+ });
64
+
65
+ it('returns valid domain with port', () => {
66
+ const result = stripProtocol('example.com:40');
67
+ assert.strictEqual(result, 'example.com:40');
68
+ });
69
+ });
70
+
71
+ describe('getActionsManifest', () => {
72
+ let fs, exitStub, consoleLog, chalk;
73
+ let DEFAULT_MANIFEST_PATH = 'path/to/manifest';
74
+
75
+ const actionMock = {
76
+ name: 'name',
77
+ category: 'Custom',
78
+ description: 'descriptoin',
79
+ type: 'function',
80
+ path: 'actions/mock.js',
81
+ entryFile: './actions/mock.ts',
82
+ parameters: [],
83
+ allowNetworks: ['127.0.0.1', 'some.domain.tld'],
84
+ };
85
+ // eslint-disable-next-line no-unused-vars
86
+ const { entryFile: _, ...resultMock } = actionMock;
87
+
88
+ fs = {
89
+ existsSync: stub(),
90
+ readFileSync: stub(),
91
+ };
92
+ chalk = {
93
+ bold: stub(),
94
+ red: stub(),
95
+ };
96
+
97
+ let { getActionsManifest } = proxyquire('./utils', { fs, chalk });
98
+
99
+ beforeEach(() => {
100
+ exitStub = stub(process, 'exit');
101
+ consoleLog = stub(console, 'log');
102
+ });
103
+ afterEach(() => {
104
+ exitStub.restore();
105
+ consoleLog.restore();
106
+ });
107
+
108
+ it('should return undefined if manifest does not exist', () => {
109
+ fs.existsSync.returns(false);
110
+
111
+ const result = getActionsManifest(DEFAULT_MANIFEST_PATH);
112
+
113
+ assert.equal(result, undefined);
114
+ });
115
+
116
+ it('should return undefined if manifest has no actions', () => {
117
+ fs.existsSync.returns(true);
118
+ fs.readFileSync.returns(JSON.stringify({ actions: [] }));
119
+
120
+ const result = getActionsManifest(DEFAULT_MANIFEST_PATH);
121
+ assert.equal(result, undefined);
122
+ });
123
+
124
+ it('should return an array of actions if manifest is valid', () => {
125
+ fs.existsSync.returns(true);
126
+ fs.readFileSync.returns(
127
+ JSON.stringify({
128
+ actions: [actionMock],
129
+ })
130
+ );
131
+
132
+ const result = getActionsManifest();
133
+
134
+ assert.deepEqual(result, [resultMock]);
135
+ assert.ok(consoleLog.called);
136
+ });
137
+
138
+ it('should strip the protocol when a domain has a protocol in allowNetworks', () => {
139
+ const mockAction = {
140
+ ...actionMock,
141
+ allowNetworks: ['http://some.domain.tld'],
142
+ };
143
+ // eslint-disable-next-line no-unused-vars
144
+ const { entryFile: _, ...resultMock } = mockAction;
145
+ fs.existsSync.returns(true);
146
+ fs.readFileSync.returns(
147
+ JSON.stringify({
148
+ actions: [mockAction],
149
+ })
150
+ );
151
+
152
+ const result = getActionsManifest();
153
+
154
+ assert.deepEqual(result, [{ ...resultMock, allowNetworks: ['some.domain.tld'] }]);
155
+ assert.ok(consoleLog.called);
156
+ });
157
+
158
+ it('should return an array of actions without entryFile prop if manifest is valid', () => {
159
+ fs.existsSync.returns(true);
160
+ fs.readFileSync.returns(
161
+ JSON.stringify({
162
+ actions: [actionMock],
163
+ })
164
+ );
165
+
166
+ const result = getActionsManifest();
167
+
168
+ assert.notDeepEqual(result, [actionMock]);
169
+ });
170
+
171
+ it('should exit with error if invalid network is found in allowNetworks', () => {
172
+ fs.existsSync.returns(true);
173
+ fs.readFileSync.returns(
174
+ JSON.stringify({
175
+ actions: [
176
+ {
177
+ name: 'action1',
178
+ entryFile: 'entry1',
179
+ allowNetworks: ['412.1.1.1'],
180
+ },
181
+ ],
182
+ })
183
+ );
184
+
185
+ getActionsManifest();
186
+
187
+ assert.ok(exitStub.calledOnceWith(1));
188
+ });
189
+
190
+ it('should exit with error if manifest is invalid JSON', () => {
191
+ fs.existsSync.returns(true);
192
+ fs.readFileSync.throws();
193
+
194
+ getActionsManifest();
195
+
196
+ assert.ok(exitStub.calledOnceWith(1));
197
+ });
198
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentful/app-scripts",
3
- "version": "1.8.2",
3
+ "version": "1.10.0",
4
4
  "description": "A collection of scripts for building Contentful Apps",
5
5
  "author": "Contentful GmbH",
6
6
  "license": "MIT",
@@ -61,5 +61,5 @@
61
61
  "open": "8.4.2",
62
62
  "ora": "5.4.1"
63
63
  },
64
- "gitHead": "43e318bc4b0ce268d1e40cf91b2116debf48e93f"
64
+ "gitHead": "f07638b628190b97813d2af4fa391b879f5f32e5"
65
65
  }