@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 +26 -0
- package/README.md +25 -1
- package/package.json +1 -1
- package/src/git-utils.js +27 -1
- package/src/server/HelixProject.js +37 -1
- package/src/server/HelixServer.js +118 -7
- package/src/up.cmd.js +8 -1
- package/src/up.js +6 -0
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
|
-
|
|
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
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
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
};
|