@adobe/aem-cli 16.13.2 → 16.15.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/CHANGELOG.md CHANGED
@@ -1,3 +1,29 @@
1
+ # [16.15.0](https://github.com/adobe/helix-cli/compare/v16.14.0...v16.15.0) (2025-09-22)
2
+
3
+
4
+ ### Features
5
+
6
+ * enable auto-reload for static HTML resources (fixes [#2400](https://github.com/adobe/helix-cli/issues/2400)) ([0866ede](https://github.com/adobe/helix-cli/commit/0866edea1579d6e1c207d756e5ee9dbe266a5e6c))
7
+ * **git-utils:** add DNS-valid tag filtering in getBranch ([fd367c4](https://github.com/adobe/helix-cli/commit/fd367c421187cf75263b32bdc5e83a886f29ef9a))
8
+ * **git-utils:** enhance DNS name validation with stricter rules and length checks ([9702a63](https://github.com/adobe/helix-cli/commit/9702a63a26949d8dce135a17192b6801508d848c))
9
+
10
+ # [16.14.0](https://github.com/adobe/helix-cli/compare/v16.13.2...v16.14.0) (2025-09-19)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * address CodeQL security alert for HTML folder handler ([76450f2](https://github.com/adobe/helix-cli/commit/76450f25c33d0300adc53fe3b6e7696974e0e384))
16
+ * address PR review comments for HTML folder feature ([c8ff512](https://github.com/adobe/helix-cli/commit/c8ff512d97227e94eb8890e083c533db92c4c663))
17
+ * address PR review comments for HTML folder security ([842650e](https://github.com/adobe/helix-cli/commit/842650e96d421a32a276573b40dfa6b7a1b3b52c))
18
+ * make HTML folder 404 test more robust for CI environment ([9f5c605](https://github.com/adobe/helix-cli/commit/9f5c60546ca22faf611eedbb3735557ebaa83c0f))
19
+ * mock both URL patterns for HTML folder proxy test ([7be689a](https://github.com/adobe/helix-cli/commit/7be689a90c7d85b451ca6c1d00a1a67b53483d68))
20
+ * update test to handle proxy behavior correctly in CI ([52f8afa](https://github.com/adobe/helix-cli/commit/52f8afa6823f4bbc2e45ade57790c2cce722cf59))
21
+
22
+
23
+ ### Features
24
+
25
+ * add --html-folder flag to serve HTML files without extensions ([682e8a2](https://github.com/adobe/helix-cli/commit/682e8a2dea8d835281a8d41eb0ef05e6c07d04b1))
26
+
1
27
  ## [16.13.2](https://github.com/adobe/helix-cli/compare/v16.13.1...v16.13.2) (2025-09-17)
2
28
 
3
29
 
package/README.md CHANGED
@@ -212,6 +212,8 @@ These proxies use a private certificate authority (CA) to sign the certificates
212
212
  servers they intercept. To make Node.js trust the server certificate, you need to add
213
213
  the CA certificate to the list of trusted CAs.
214
214
 
215
+ ### Option 1: Using IT-provided CA certificate
216
+
215
217
  The CA certificate is typically provided by your IT department. You can ask them for
216
218
  the CA certificate and save it to a file, e.g. `my-ca.crt`.
217
219
 
@@ -223,7 +225,29 @@ export NODE_EXTRA_CA_CERTS=my-ca.crt
223
225
  aem up
224
226
  ```
225
227
 
226
- This will make Node.js trust the server certificate and `aem up` should work.
228
+ ### Option 2: Extracting certificate from browser
229
+
230
+ If you don't have access to the CA certificate from your IT department, you can extract it directly
231
+ from your browser:
232
+
233
+ 1. Access the AEM admin URL in your browser (e.g., `https://admin.hlx.page/sidekick/owner-name/repo-name/github-branch-name/config.json`)
234
+ 2. Click on the padlock icon in the address bar and view the certificate details
235
+ 3. Export the certificate chain as a Base64 encoded `.pem` file
236
+ 4. Save it to a directory (e.g., `certs/hlx.page.pem`)
237
+ 5. Set the environment variable and run aem:
238
+
239
+ ```bash
240
+ export NODE_EXTRA_CA_CERTS=./certs/hlx.page.pem
241
+ aem up
242
+ ```
243
+
244
+ On Windows, use `set` instead of `export`:
245
+ ```cmd
246
+ set NODE_EXTRA_CA_CERTS=./certs/hlx.page.pem
247
+ aem up
248
+ ```
249
+
250
+ Either approach will make Node.js trust the server certificate and `aem up` should work.
227
251
 
228
252
  ## `npm install` fails with `File exists: /opt/homebrew/bin/hlx`
229
253
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/aem-cli",
3
- "version": "16.13.2",
3
+ "version": "16.15.0",
4
4
  "description": "AEM CLI",
5
5
  "main": "index.js",
6
6
  "type": "module",
package/src/git-utils.js CHANGED
@@ -135,6 +135,28 @@ export default class GitUtils {
135
135
  return false;
136
136
  }
137
137
 
138
+ /**
139
+ * Checks if a name is valid for use in a DNS subdomain.
140
+ * DNS labels must:
141
+ * - Be between 1 and 63 characters long
142
+ * - Contain only alphanumeric characters and hyphens
143
+ * - Not start or end with a hyphen
144
+ * - Not contain dots or other special characters
145
+ * @param {string} name the name to validate
146
+ * @returns {boolean} true if valid for DNS
147
+ */
148
+ static isValidDNSName(name) {
149
+ // Check length (DNS labels must be 1-63 characters)
150
+ if (!name || name.length > 63) {
151
+ return false;
152
+ }
153
+
154
+ // DNS labels can only contain alphanumeric characters and hyphens
155
+ // They cannot start or end with hyphens
156
+ // They cannot contain dots or other special characters
157
+ return /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/.test(name);
158
+ }
159
+
138
160
  /**
139
161
  * Returns the name of the current branch. If `HEAD` is at a tag, the name of the tag
140
162
  * will be returned instead, if head is at a commit, fallback will be returned.
@@ -178,7 +200,11 @@ export default class GitUtils {
178
200
  ? await git.resolveRef({ fs, dir, ref: obj.object.object }) // annotated tag
179
201
  : oid; // lightweight tag
180
202
  if (commitSha === rev) {
181
- return tag;
203
+ // Only return tags that are valid for DNS names
204
+ // Skip tags containing dots or other invalid characters
205
+ if (GitUtils.isValidDNSName(tag)) {
206
+ return tag;
207
+ }
182
208
  }
183
209
  }
184
210
 
@@ -9,7 +9,7 @@
9
9
  * OF ANY KIND, either express or implied. See the License for the specific language
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
- import { resolve } from 'path';
12
+ import path, { resolve } from 'path';
13
13
  import { lstat } from 'fs/promises';
14
14
  import { HelixServer } from './HelixServer.js';
15
15
  import { BaseProject } from './BaseProject.js';
@@ -78,6 +78,22 @@ export class HelixProject extends BaseProject {
78
78
  return this;
79
79
  }
80
80
 
81
+ withHtmlFolder(value) {
82
+ if (value) {
83
+ // Security: reject any paths with traversal patterns or absolute paths
84
+ if (path.isAbsolute(value) || value.includes('..') || value.startsWith('/')) {
85
+ throw new Error(`Invalid HTML folder name: ${value} only folders within the current workspace are allowed`);
86
+ }
87
+
88
+ this._htmlFolder = value;
89
+ this._server.withHtmlFolder(value);
90
+ } else {
91
+ this._htmlFolder = value;
92
+ this._server.withHtmlFolder(value);
93
+ }
94
+ return this;
95
+ }
96
+
81
97
  get proxyUrl() {
82
98
  return this._proxyUrl;
83
99
  }
@@ -115,6 +131,10 @@ export class HelixProject extends BaseProject {
115
131
  return this._headHtml;
116
132
  }
117
133
 
134
+ get htmlFolder() {
135
+ return this._htmlFolder;
136
+ }
137
+
118
138
  async init() {
119
139
  await super.init();
120
140
  this._indexer = new Indexer()
@@ -161,11 +181,27 @@ export class HelixProject extends BaseProject {
161
181
  }
162
182
  }
163
183
 
184
+ async initHtmlFolder() {
185
+ if (this._htmlFolder && this.liveReload) {
186
+ const htmlFolderPath = resolve(this.directory, this._htmlFolder);
187
+ try {
188
+ await lstat(htmlFolderPath);
189
+ this.log.debug(`Registered HTML folder for live-reload: ${this._htmlFolder}`);
190
+ // Watch all HTML files in the folder - only .html extension
191
+ this.liveReload.registerFiles([`${htmlFolderPath}/**/*.html`], `/${this._htmlFolder}/`);
192
+ } catch (e) {
193
+ this.log.error(`HTML folder '${this._htmlFolder}' does not exist`);
194
+ throw new Error(`HTML folder '${this._htmlFolder}' does not exist`);
195
+ }
196
+ }
197
+ }
198
+
164
199
  async start() {
165
200
  this.log.debug('Launching AEM dev server...');
166
201
  await super.start();
167
202
  await this.initHeadHtml();
168
203
  await this.init404Html();
204
+ await this.initHtmlFolder();
169
205
  if (this._indexer) {
170
206
  await this._indexer.init();
171
207
  }
@@ -13,6 +13,7 @@ import crypto from 'crypto';
13
13
  import express from 'express';
14
14
  import { promisify } from 'util';
15
15
  import path from 'path';
16
+ import { lstat, readFile } from 'fs/promises';
16
17
  import compression from 'compression';
17
18
  import utils from './utils.js';
18
19
  import RequestContext from './RequestContext.js';
@@ -62,6 +63,12 @@ export class HelixServer extends BaseServer {
62
63
  return this;
63
64
  }
64
65
 
66
+ withHtmlFolder(value) {
67
+ // It's now sanitized in HelixProject.withHtmlFolder
68
+ this._htmlFolder = value;
69
+ return this;
70
+ }
71
+
65
72
  async handleLogin(req, res) {
66
73
  // disable autologin if login was called at least once
67
74
  this._autoLogin = false;
@@ -147,6 +154,86 @@ export class HelixServer extends BaseServer {
147
154
  .send('');
148
155
  }
149
156
 
157
+ /**
158
+ * HTML Folder handler - serves HTML files without extensions
159
+ * @param {Express.Request} req request
160
+ * @param {Express.Response} res response
161
+ * @param {Function} next next middleware
162
+ */
163
+ async handleHtmlFolderRequest(req, res, next) {
164
+ if (!this._htmlFolder) {
165
+ return next();
166
+ }
167
+
168
+ // Use Express's req.path for pathname extraction
169
+ const pathname = req.path;
170
+ const folderPrefix = `/${this._htmlFolder}/`;
171
+
172
+ // Check if the request is for the HTML folder
173
+ if (!pathname.startsWith(folderPrefix)) {
174
+ return next();
175
+ }
176
+
177
+ // Extract the path within the HTML folder
178
+ const relativePath = pathname.slice(folderPrefix.length);
179
+
180
+ // Security check: prevent path traversal with /../ anywhere in the path
181
+ if (relativePath.includes('/../')) {
182
+ return next();
183
+ }
184
+
185
+ // Don't process if it already has an extension
186
+ if (relativePath.includes('.')) {
187
+ return next();
188
+ }
189
+
190
+ // Build the HTML file path - only support .html extension
191
+ const htmlFile = path.join(this._project.directory, this._htmlFolder, `${relativePath}.html`);
192
+
193
+ // Security check: ensure the file is within the project directory
194
+ const relPath = path.relative(this._project.directory, htmlFile);
195
+ if (relPath.startsWith('..') || path.isAbsolute(relPath)) {
196
+ return next();
197
+ }
198
+
199
+ // Check if the HTML file exists and is a file
200
+ try {
201
+ const stats = await lstat(htmlFile);
202
+ if (!stats.isFile()) {
203
+ return next();
204
+ }
205
+ } catch (e) {
206
+ // File doesn't exist, continue to next handler
207
+ return next();
208
+ }
209
+
210
+ const sendFile = promisify(res.sendFile).bind(res);
211
+ const { log } = this;
212
+ const liveReload = this._liveReload;
213
+
214
+ // Register for live reload if enabled
215
+ if (liveReload) {
216
+ liveReload.startRequest(req.id, req.url);
217
+ }
218
+
219
+ // Serve the file
220
+ await sendFile(htmlFile, {
221
+ dotfiles: 'deny',
222
+ headers: {
223
+ 'access-control-allow-origin': '*',
224
+ 'content-type': 'text/html; charset=utf-8',
225
+ },
226
+ });
227
+
228
+ if (liveReload) {
229
+ liveReload.registerFile(req.id, htmlFile);
230
+ liveReload.endRequest(req.id);
231
+ }
232
+
233
+ log.debug(`served HTML file ${htmlFile} for ${req.url}`);
234
+ return undefined;
235
+ }
236
+
150
237
  /**
151
238
  * Proxy Mode route handler
152
239
  * @param {Express.Request} req request
@@ -183,16 +270,32 @@ export class HelixServer extends BaseServer {
183
270
 
184
271
  // try to serve static
185
272
  try {
186
- await sendFile(filePath, {
187
- dotfiles: 'allow',
188
- headers: {
273
+ // Check if it's an HTML file and live reload is enabled
274
+ if (liveReload && filePath.endsWith('.html')) {
275
+ // Read the HTML file and inject the livereload script
276
+ let htmlContent = await readFile(filePath, 'utf-8');
277
+ htmlContent = utils.injectLiveReloadScript(htmlContent, this);
278
+
279
+ res.set({
280
+ 'content-type': 'text/html; charset=utf-8',
189
281
  'access-control-allow-origin': '*',
190
- },
191
- });
192
- if (liveReload) {
282
+ });
283
+ res.send(htmlContent);
193
284
  liveReload.registerFile(ctx.requestId, filePath);
285
+ log.debug(`${pfx}served local HTML file with livereload: ${filePath}`);
286
+ } else {
287
+ // Serve other files normally
288
+ await sendFile(filePath, {
289
+ dotfiles: 'allow',
290
+ headers: {
291
+ 'access-control-allow-origin': '*',
292
+ },
293
+ });
294
+ if (liveReload) {
295
+ liveReload.registerFile(ctx.requestId, filePath);
296
+ }
297
+ log.debug(`${pfx}served local file ${filePath}`);
194
298
  }
195
- log.debug(`${pfx}served local file ${filePath}`);
196
299
  return;
197
300
  } catch (e) {
198
301
  log.debug(`${pfx}unable to deliver local file ${ctx.path} - ${e.stack || e}`);
@@ -244,6 +347,14 @@ export class HelixServer extends BaseServer {
244
347
  this.app.post(LOGIN_ACK_ROUTE, express.json(), asyncHandler(this.handleLoginAck.bind(this)));
245
348
  this.app.options(LOGIN_ACK_ROUTE, asyncHandler(this.handleLoginAck.bind(this)));
246
349
 
350
+ // Add HTML folder handler before the general proxy handler
351
+ if (this._htmlFolder) {
352
+ // Only handle GET requests for the HTML folder path
353
+ const htmlFolderPattern = new RegExp(`^/${this._htmlFolder}/.*`);
354
+ this.app.get(htmlFolderPattern, asyncHandler(this.handleHtmlFolderRequest.bind(this)));
355
+ this.log.info(`Serving HTML files from folder: ${this._htmlFolder}`);
356
+ }
357
+
247
358
  const handler = asyncHandler(this.handleProxyModeRequest.bind(this));
248
359
  this.app.get(/.*/, handler);
249
360
  this.app.post(/.*/, handler);
package/src/up.cmd.js CHANGED
@@ -54,6 +54,12 @@ export default class UpCommand extends AbstractServerCommand {
54
54
  return this;
55
55
  }
56
56
 
57
+ withHtmlFolder(value) {
58
+ // Basic validation - detailed validation done in HelixProject
59
+ this._htmlFolder = value;
60
+ return this;
61
+ }
62
+
57
63
  async doStop() {
58
64
  await super.doStop();
59
65
  if (this._watcher) {
@@ -105,7 +111,8 @@ export default class UpCommand extends AbstractServerCommand {
105
111
  .withPrintIndex(this._printIndex)
106
112
  .withAllowInsecure(this._allowInsecure)
107
113
  .withSiteToken(this._siteToken)
108
- .withCookies(this._cookies);
114
+ .withCookies(this._cookies)
115
+ .withHtmlFolder(this._htmlFolder);
109
116
 
110
117
  this.log.info(chalk`{yellow ___ ________ ___ __ __ v${pkgJson.version}}`);
111
118
  this.log.info(chalk`{yellow / | / ____/ |/ / _____(_)___ ___ __ __/ /___ _/ /_____ _____}`);
package/src/up.js CHANGED
@@ -115,6 +115,11 @@ export default function up() {
115
115
  type: 'boolean',
116
116
  default: false,
117
117
  })
118
+ .option('html-folder', {
119
+ alias: 'htmlFolder',
120
+ describe: 'Serve HTML files from this folder without extensions (e.g., /folder/file serves folder/file.html) use this to preview content changes if you do not have access to the authoring system',
121
+ type: 'string',
122
+ })
118
123
 
119
124
  .help();
120
125
  },
@@ -141,6 +146,7 @@ export default function up() {
141
146
  .withKill(argv.stopOther)
142
147
  .withCache(argv.alphaCache)
143
148
  .withCookies(argv.cookies)
149
+ .withHtmlFolder(argv.htmlFolder)
144
150
  .run();
145
151
  },
146
152
  };