@contentful/app-scripts 2.5.10 → 2.5.11

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.
@@ -9,6 +9,7 @@ export declare function resolvePaths(localPath: string): {
9
9
  localFunctionsPath: string;
10
10
  };
11
11
  export declare function cloneAndResolveManifests(cloneURL: string, localTmpPath: string, localPath: string, localFunctionsPath: string, keepPackageJson?: boolean): Promise<void>;
12
- export declare function clone(cloneURL: string, localFunctionsPath: string): Promise<any>;
12
+ export declare function removeIgnoredFiles(localTmpPath: string, ignoredFiles: string[]): void;
13
+ export declare function clone(cloneURL: string, localFunctionsPath: string): Promise<void>;
13
14
  export declare function mergeAppManifest(localPath: string, localTmpPath: string): Promise<void>;
14
15
  export declare function updatePackageJsonWithBuild(localPath: string, localTmpPath: string): Promise<void>;
@@ -3,10 +3,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.updatePackageJsonWithBuild = exports.mergeAppManifest = exports.clone = exports.cloneAndResolveManifests = exports.resolvePaths = exports.renameClonedFiles = exports.moveFilesToFinalDirectory = exports.touchupAppManifest = exports.getCloneURL = exports.cloneFunction = void 0;
7
- // eslint-disable-next-line @typescript-eslint/no-var-requires
8
- const tiged = require('tiged');
6
+ exports.updatePackageJsonWithBuild = exports.mergeAppManifest = exports.clone = exports.removeIgnoredFiles = exports.cloneAndResolveManifests = exports.resolvePaths = exports.renameClonedFiles = exports.moveFilesToFinalDirectory = exports.touchupAppManifest = exports.getCloneURL = exports.cloneFunction = void 0;
9
7
  const node_fs_1 = __importDefault(require("node:fs"));
8
+ const node_child_process_1 = require("node:child_process");
9
+ const node_os_1 = require("node:os");
10
10
  const chalk_1 = __importDefault(require("chalk"));
11
11
  const node_path_1 = require("node:path");
12
12
  const constants_1 = require("./constants");
@@ -88,13 +88,13 @@ function renameClonedFiles(localTmpPath, settings) {
88
88
  }
89
89
  exports.renameClonedFiles = renameClonedFiles;
90
90
  function resolvePaths(localPath) {
91
- const localTmpPath = (0, node_path_1.resolve)(localPath, 'tmp'); // we require a tmp directory because tiged overwrites all files in the target directory
91
+ const localTmpPath = (0, node_path_1.resolve)(localPath, 'tmp');
92
92
  const localFunctionsPath = (0, node_path_1.resolve)(localPath, 'functions');
93
93
  return { localTmpPath, localFunctionsPath };
94
94
  }
95
95
  exports.resolvePaths = resolvePaths;
96
96
  async function cloneAndResolveManifests(cloneURL, localTmpPath, localPath, localFunctionsPath, keepPackageJson = false) {
97
- const tigedInstance = await clone(cloneURL, localTmpPath);
97
+ await clone(cloneURL, localTmpPath);
98
98
  // merge the manifest from the template folder to the root folder
99
99
  await mergeAppManifest(localPath, localTmpPath);
100
100
  // create a deep copy of the IGNORED_CLONED_FILES array
@@ -110,18 +110,85 @@ async function cloneAndResolveManifests(cloneURL, localTmpPath, localPath, local
110
110
  ignoredFiles.push('tsconfig.json');
111
111
  }
112
112
  // remove the cloned files that we've already merged
113
- await tigedInstance.remove("unused_param", localTmpPath, {
114
- action: 'remove',
115
- files: ignoredFiles.map((fileName) => `${localTmpPath}/${fileName}`),
116
- });
113
+ removeIgnoredFiles(localTmpPath, ignoredFiles);
117
114
  }
118
115
  exports.cloneAndResolveManifests = cloneAndResolveManifests;
116
+ function removeIgnoredFiles(localTmpPath, ignoredFiles) {
117
+ for (const fileName of ignoredFiles) {
118
+ const filePath = (0, node_path_1.resolve)(localTmpPath, fileName);
119
+ if (node_fs_1.default.existsSync(filePath)) {
120
+ node_fs_1.default.rmSync(filePath, { recursive: true, force: true });
121
+ }
122
+ }
123
+ }
124
+ exports.removeIgnoredFiles = removeIgnoredFiles;
125
+ // Parse the trusted REPO_URL constant to extract canonical owner, repo, and base path
126
+ // REPO_URL format: https://github.com/contentful/apps/function-examples
127
+ const REPO_URL_MATCH = constants_1.REPO_URL.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)\/(.+)$/);
128
+ if (!REPO_URL_MATCH) {
129
+ throw new Error('Invalid REPO_URL constant configuration');
130
+ }
131
+ const [, CANONICAL_OWNER, CANONICAL_REPO, CANONICAL_BASE_PATH] = REPO_URL_MATCH;
132
+ const CANONICAL_REPO_URL = `https://github.com/${CANONICAL_OWNER}/${CANONICAL_REPO}.git`;
133
+ // Strict validation regex for subfolder path segments (alphanumeric, dash, underscore only)
134
+ const SAFE_PATH_SEGMENT_REGEX = /^[a-zA-Z0-9_-]+$/;
119
135
  async function clone(cloneURL, localFunctionsPath) {
120
- const tigedInstance = tiged(cloneURL, { mode: 'tar', disableCache: true, force: true });
121
- await tigedInstance.clone(localFunctionsPath);
122
- return tigedInstance;
136
+ // Validate that cloneURL starts with the trusted REPO_URL
137
+ if (!cloneURL.startsWith(constants_1.REPO_URL)) {
138
+ throw new Error(`Invalid clone URL: must start with ${constants_1.REPO_URL}`);
139
+ }
140
+ // Extract only the user-supplied portion (example/language) after REPO_URL
141
+ const userSuppliedPath = cloneURL.slice(constants_1.REPO_URL.length);
142
+ // Remove leading slash if present and validate format
143
+ const trimmedPath = userSuppliedPath.startsWith('/') ? userSuppliedPath.slice(1) : userSuppliedPath;
144
+ if (!trimmedPath) {
145
+ throw new Error('Invalid clone URL: missing example/language path');
146
+ }
147
+ // Validate each path segment for safe characters only
148
+ const pathSegments = trimmedPath.split('/');
149
+ for (const segment of pathSegments) {
150
+ if (!segment || !SAFE_PATH_SEGMENT_REGEX.test(segment)) {
151
+ throw new Error(`Invalid clone URL: path segment "${segment}" contains unsafe characters`);
152
+ }
153
+ }
154
+ // Build the full subfolder path from the trusted base + validated user path
155
+ const subfolderPath = `${CANONICAL_BASE_PATH}/${trimmedPath}`;
156
+ const tempDir = (0, node_path_1.resolve)((0, node_os_1.tmpdir)(), `contentful-clone-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`);
157
+ try {
158
+ // Clone using ONLY the canonical repo URL derived from trusted REPO_URL constant
159
+ // execFileSync with array args prevents shell injection
160
+ (0, node_child_process_1.execFileSync)('git', ['clone', '--depth', '1', CANONICAL_REPO_URL, tempDir], { stdio: 'ignore' });
161
+ // Create destination directory
162
+ if (!node_fs_1.default.existsSync(localFunctionsPath)) {
163
+ node_fs_1.default.mkdirSync(localFunctionsPath, { recursive: true });
164
+ }
165
+ // Copy the subfolder contents to destination
166
+ const sourcePath = (0, node_path_1.resolve)(tempDir, subfolderPath);
167
+ if (!node_fs_1.default.existsSync(sourcePath)) {
168
+ throw new Error(`Subfolder not found: ${subfolderPath}`);
169
+ }
170
+ // Copy files using native fs.cpSync (Node 16.7+, safe from injection)
171
+ copyDirectoryContents(sourcePath, localFunctionsPath);
172
+ }
173
+ finally {
174
+ // Clean up temp directory using native fs (Node 14.14+)
175
+ try {
176
+ node_fs_1.default.rmSync(tempDir, { recursive: true, force: true });
177
+ }
178
+ catch {
179
+ // Ignore cleanup errors - temp directory will be cleaned by OS eventually
180
+ }
181
+ }
123
182
  }
124
183
  exports.clone = clone;
184
+ function copyDirectoryContents(sourcePath, destPath) {
185
+ const entries = node_fs_1.default.readdirSync(sourcePath, { withFileTypes: true });
186
+ for (const entry of entries) {
187
+ const srcFile = (0, node_path_1.resolve)(sourcePath, entry.name);
188
+ const destFile = (0, node_path_1.resolve)(destPath, entry.name);
189
+ node_fs_1.default.cpSync(srcFile, destFile, { recursive: true });
190
+ }
191
+ }
125
192
  async function mergeAppManifest(localPath, localTmpPath) {
126
193
  const finalAppManifestType = await (0, file_1.exists)((0, node_path_1.resolve)(localPath, constants_1.CONTENTFUL_APP_MANIFEST));
127
194
  const tmpAppManifestType = await (0, file_1.whichExists)(localTmpPath, [constants_1.CONTENTFUL_APP_MANIFEST, constants_1.APP_MANIFEST]); // find the app manifest in the cloned files
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentful/app-scripts",
3
- "version": "2.5.10",
3
+ "version": "2.5.11",
4
4
  "description": "A collection of scripts for building Contentful Apps",
5
5
  "author": "Contentful GmbH",
6
6
  "license": "MIT",
@@ -49,7 +49,7 @@
49
49
  "dependencies": {
50
50
  "@esbuild-plugins/node-globals-polyfill": "^0.2.3",
51
51
  "@esbuild-plugins/node-modules-polyfill": "^0.2.2",
52
- "@segment/analytics-node": "^2.0.0",
52
+ "@segment/analytics-node": "^3.0.0",
53
53
  "adm-zip": "0.5.16",
54
54
  "axios": "^1.13.5",
55
55
  "bottleneck": "2.19.5",
@@ -57,19 +57,18 @@
57
57
  "commander": "12.1.0",
58
58
  "contentful-management": "^11.48.1",
59
59
  "dotenv": "17.3.1",
60
- "esbuild": "^0.27.0",
60
+ "esbuild": "^0.27.4",
61
61
  "ignore": "7.0.5",
62
62
  "inquirer": "8.2.7",
63
- "lodash": "4.17.23",
63
+ "lodash": "4.18.1",
64
64
  "merge-options": "^3.0.4",
65
65
  "open": "8.4.2",
66
66
  "ora": "5.4.1",
67
- "tiged": "^2.12.7",
68
67
  "zod": "^3.24.1"
69
68
  },
70
69
  "gitHead": "4c3506be3f52c7a8aae17deaa75acefc9a805b42",
71
70
  "devDependencies": {
72
- "@types/adm-zip": "0.5.7",
71
+ "@types/adm-zip": "0.5.8",
73
72
  "@types/analytics-node": "3.1.14",
74
73
  "@types/chai": "4.3.16",
75
74
  "@types/inquirer": "8.2.1",
@@ -82,7 +81,7 @@
82
81
  "mocha": "11.7.5",
83
82
  "proxyquire": "2.1.3",
84
83
  "rimraf": "6.1.3",
85
- "sinon": "21.0.1",
84
+ "sinon": "21.0.3",
86
85
  "ts-mocha": "11.1.0",
87
86
  "ts-node": "10.9.2"
88
87
  }