@adobe/aem-cli 16.8.6 → 16.9.1
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 +14 -0
- package/package.json +5 -4
- package/src/cli.js +1 -1
- package/src/config/config-utils.js +74 -1
- package/src/server/HeadHtmlSupport.js +4 -0
- package/src/server/HelixProject.js +28 -0
- package/src/server/HelixServer.js +101 -1
- package/src/server/utils.js +15 -2
- package/src/up.cmd.js +26 -0
- package/src/up.js +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
## [16.9.1](https://github.com/adobe/helix-cli/compare/v16.9.0...v16.9.1) (2025-01-13)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **deps:** update external fixes ([#2476](https://github.com/adobe/helix-cli/issues/2476)) ([d7b04fa](https://github.com/adobe/helix-cli/commit/d7b04faef82a4304ae9634f283de78aff57048b2))
|
|
7
|
+
|
|
8
|
+
# [16.9.0](https://github.com/adobe/helix-cli/compare/v16.8.6...v16.9.0) (2025-01-13)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* Allow AEM CLI to obtain site token ([#2471](https://github.com/adobe/helix-cli/issues/2471)) ([a937bc7](https://github.com/adobe/helix-cli/commit/a937bc7fe1b28ea6221dfdeb4c4f547a355991aa))
|
|
14
|
+
|
|
1
15
|
## [16.8.6](https://github.com/adobe/helix-cli/compare/v16.8.5...v16.8.6) (2025-01-06)
|
|
2
16
|
|
|
3
17
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adobe/aem-cli",
|
|
3
|
-
"version": "16.
|
|
3
|
+
"version": "16.9.1",
|
|
4
4
|
"description": "AEM CLI",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -53,14 +53,15 @@
|
|
|
53
53
|
"express": "4.21.2",
|
|
54
54
|
"faye-websocket": "0.11.4",
|
|
55
55
|
"fs-extra": "11.2.0",
|
|
56
|
-
"glob": "11.0.
|
|
56
|
+
"glob": "11.0.1",
|
|
57
57
|
"glob-to-regexp": "0.4.1",
|
|
58
58
|
"hast-util-select": "6.0.3",
|
|
59
59
|
"http-proxy-agent": "7.0.2",
|
|
60
60
|
"https-proxy-agent": "7.0.6",
|
|
61
|
-
"ignore": "7.0.
|
|
61
|
+
"ignore": "7.0.1",
|
|
62
62
|
"ini": "5.0.0",
|
|
63
63
|
"isomorphic-git": "1.29.0",
|
|
64
|
+
"jose": "5.9.6",
|
|
64
65
|
"livereload-js": "4.0.2",
|
|
65
66
|
"node-fetch": "3.3.2",
|
|
66
67
|
"open": "10.1.0",
|
|
@@ -71,7 +72,7 @@
|
|
|
71
72
|
"semver": "7.6.3",
|
|
72
73
|
"shelljs": "0.8.5",
|
|
73
74
|
"unified": "11.0.5",
|
|
74
|
-
"uuid": "11.0.
|
|
75
|
+
"uuid": "11.0.5",
|
|
75
76
|
"yargs": "17.7.2"
|
|
76
77
|
},
|
|
77
78
|
"devDependencies": {
|
package/src/cli.js
CHANGED
|
@@ -10,7 +10,12 @@
|
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
import chalk from 'chalk-template';
|
|
13
|
+
import fs from 'fs/promises';
|
|
14
|
+
import fse from 'fs-extra';
|
|
15
|
+
import os from 'os';
|
|
16
|
+
import path from 'path';
|
|
13
17
|
import semver from 'semver';
|
|
18
|
+
import { decodeJwt } from 'jose';
|
|
14
19
|
import GitUtils from '../git-utils.js';
|
|
15
20
|
import pkgJson from '../package.cjs';
|
|
16
21
|
|
|
@@ -21,14 +26,82 @@ import pkgJson from '../package.cjs';
|
|
|
21
26
|
*/
|
|
22
27
|
export async function validateDotEnv(dir = process.cwd()) {
|
|
23
28
|
if (await GitUtils.isIgnored(dir, '.env')) {
|
|
24
|
-
return;
|
|
29
|
+
return true;
|
|
25
30
|
}
|
|
26
31
|
process.stdout.write(chalk`
|
|
27
32
|
{yellowBright Warning:} Your {cyan '.env'} file is currently not ignored by git.
|
|
28
33
|
This is typically not good because it might contain secrets
|
|
29
34
|
which should never be stored in the git repository.
|
|
35
|
+
`);
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const hlxFolder = '.hlx';
|
|
40
|
+
const tokenFileName = '.hlx-token';
|
|
41
|
+
const tokenFilePath = path.join(hlxFolder, tokenFileName);
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Writes the site token to the .hlx/.hlx-token file.
|
|
45
|
+
* Checks if the .hlx file is ignored by git and adds it to the .gitignore file if necessary.
|
|
46
|
+
*
|
|
47
|
+
* @param {string} siteToken
|
|
48
|
+
*/
|
|
49
|
+
export async function saveSiteTokenToFile(siteToken) {
|
|
50
|
+
if (!siteToken) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/*
|
|
55
|
+
don't allow writing arbitrary data to the file system.
|
|
56
|
+
validate and write only valid site tokens to the file
|
|
57
|
+
*/
|
|
58
|
+
if (siteToken.startsWith('hlxtst_')) {
|
|
59
|
+
try {
|
|
60
|
+
decodeJwt(siteToken.substring(7));
|
|
61
|
+
} catch (e) {
|
|
62
|
+
process.stdout.write(chalk`
|
|
63
|
+
{redBright Error:} The provided site token is not a valid JWT, it will not be written to your .hlx-token file.
|
|
64
|
+
`);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
process.stdout.write(chalk`
|
|
69
|
+
{redBright Error:} The provided site token is not a recognised token format, it will not be written to your .hlx-token file.
|
|
70
|
+
`);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
await fs.mkdir(hlxFolder, { recursive: true });
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
await fs.writeFile(tokenFilePath, JSON.stringify({ siteToken }, null, 2), 'utf8');
|
|
78
|
+
} finally {
|
|
79
|
+
if (!(await GitUtils.isIgnored(process.cwd(), tokenFilePath))) {
|
|
80
|
+
await fs.appendFile('.gitignore', `${os.EOL}${tokenFileName}${os.EOL}`, 'utf8');
|
|
81
|
+
process.stdout.write(chalk`
|
|
82
|
+
{redBright Warning:} Added your {cyan '.hlx-token'} file to .gitignore, because it now contains your token.
|
|
83
|
+
Please make sure the token is not stored in the git repository.
|
|
84
|
+
`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function getSiteTokenFromFile() {
|
|
90
|
+
if (!(await fse.pathExists(tokenFilePath))) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
30
93
|
|
|
94
|
+
try {
|
|
95
|
+
const tokenInfo = JSON.parse(await fs.readFile(tokenFilePath, 'utf8'));
|
|
96
|
+
return tokenInfo.siteToken;
|
|
97
|
+
} catch (e) {
|
|
98
|
+
process.stdout.write(chalk`
|
|
99
|
+
{redBright Error:} The site token could not be read from the {cyan '.hlx-token'} file.
|
|
31
100
|
`);
|
|
101
|
+
process.stdout.write(`${e.stack}\n`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return null;
|
|
32
105
|
}
|
|
33
106
|
|
|
34
107
|
/**
|
|
@@ -33,10 +33,26 @@ export class HelixProject extends BaseProject {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
withSiteToken(value) {
|
|
36
|
+
this.siteToken = value;
|
|
36
37
|
this._server.withSiteToken(value);
|
|
37
38
|
return this;
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
withSite(site) {
|
|
42
|
+
this._site = site;
|
|
43
|
+
return this;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
withOrg(org) {
|
|
47
|
+
this._org = org;
|
|
48
|
+
return this;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
withSiteLoginUrl(value) {
|
|
52
|
+
this._siteLoginUrl = value;
|
|
53
|
+
return this;
|
|
54
|
+
}
|
|
55
|
+
|
|
40
56
|
withProxyUrl(value) {
|
|
41
57
|
this._proxyUrl = value;
|
|
42
58
|
return this;
|
|
@@ -69,6 +85,18 @@ export class HelixProject extends BaseProject {
|
|
|
69
85
|
return this._server._liveReload;
|
|
70
86
|
}
|
|
71
87
|
|
|
88
|
+
get org() {
|
|
89
|
+
return this._org;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
get site() {
|
|
93
|
+
return this._site;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
get siteLoginUrl() {
|
|
97
|
+
return this._siteLoginUrl;
|
|
98
|
+
}
|
|
99
|
+
|
|
72
100
|
get file404html() {
|
|
73
101
|
return this._file404html;
|
|
74
102
|
}
|
|
@@ -9,6 +9,8 @@
|
|
|
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 crypto from 'crypto';
|
|
13
|
+
import express from 'express';
|
|
12
14
|
import { promisify } from 'util';
|
|
13
15
|
import path from 'path';
|
|
14
16
|
import compression from 'compression';
|
|
@@ -16,6 +18,10 @@ import utils from './utils.js';
|
|
|
16
18
|
import RequestContext from './RequestContext.js';
|
|
17
19
|
import { asyncHandler, BaseServer } from './BaseServer.js';
|
|
18
20
|
import LiveReload from './LiveReload.js';
|
|
21
|
+
import { saveSiteTokenToFile } from '../config/config-utils.js';
|
|
22
|
+
|
|
23
|
+
const LOGIN_ROUTE = '/.aem/cli/login';
|
|
24
|
+
const LOGIN_ACK_ROUTE = '/.aem/cli/login/ack';
|
|
19
25
|
|
|
20
26
|
export class HelixServer extends BaseServer {
|
|
21
27
|
/**
|
|
@@ -27,6 +33,7 @@ export class HelixServer extends BaseServer {
|
|
|
27
33
|
this._liveReload = null;
|
|
28
34
|
this._enableLiveReload = false;
|
|
29
35
|
this._app.use(compression());
|
|
36
|
+
this._autoLogin = true;
|
|
30
37
|
}
|
|
31
38
|
|
|
32
39
|
withLiveReload(value) {
|
|
@@ -39,6 +46,91 @@ export class HelixServer extends BaseServer {
|
|
|
39
46
|
return this;
|
|
40
47
|
}
|
|
41
48
|
|
|
49
|
+
async handleLogin(req, res) {
|
|
50
|
+
// disable autologin if login was called at least once
|
|
51
|
+
this._autoLogin = false;
|
|
52
|
+
// clear any previous login errors
|
|
53
|
+
delete this.loginError;
|
|
54
|
+
|
|
55
|
+
if (!this._project.siteLoginUrl) {
|
|
56
|
+
res.status(404).send('Login not supported. Could not extract site and org information.');
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
this.log.info(`Starting login process for : ${this._project.org}/${this._project.site}. Redirecting...`);
|
|
61
|
+
this._loginState = crypto.randomUUID();
|
|
62
|
+
const loginUrl = `${this._project.siteLoginUrl}&state=${this._loginState}`;
|
|
63
|
+
res.status(302).set('location', loginUrl).send('');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async handleLoginAck(req, res) {
|
|
67
|
+
const CACHE_CONTROL = 'no-store, private, must-revalidate';
|
|
68
|
+
const CORS_HEADERS = {
|
|
69
|
+
'access-control-allow-methods': 'POST, OPTIONS',
|
|
70
|
+
'access-control-allow-headers': 'content-type',
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const { origin } = req.headers;
|
|
74
|
+
if (['https://admin.hlx.page', 'https://admin-ci.hlx.page'].includes(origin)) {
|
|
75
|
+
CORS_HEADERS['access-control-allow-origin'] = origin;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (req.method === 'OPTIONS') {
|
|
79
|
+
res.status(200).set(CORS_HEADERS).send('');
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (req.method === 'POST') {
|
|
84
|
+
const { state, siteToken } = req.body;
|
|
85
|
+
try {
|
|
86
|
+
if (!this._loginState || this._loginState !== state) {
|
|
87
|
+
this.loginError = { message: 'Login Failed: We received an invalid state.' };
|
|
88
|
+
this.log.warn('State mismatch. Discarding site token.');
|
|
89
|
+
res.status(400)
|
|
90
|
+
.set(CORS_HEADERS)
|
|
91
|
+
.set('cache-control', CACHE_CONTROL)
|
|
92
|
+
.send('Invalid state');
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!siteToken) {
|
|
97
|
+
this.loginError = { message: 'Login Failed: Missing site token.' };
|
|
98
|
+
res.status(400)
|
|
99
|
+
.set('cache-control', CACHE_CONTROL)
|
|
100
|
+
.set(CORS_HEADERS)
|
|
101
|
+
.send('Missing site token');
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
this.withSiteToken(siteToken);
|
|
106
|
+
this._project.headHtml.setSiteToken(siteToken);
|
|
107
|
+
await saveSiteTokenToFile(siteToken);
|
|
108
|
+
this.log.info('Site token received and saved to file.');
|
|
109
|
+
|
|
110
|
+
res.status(200)
|
|
111
|
+
.set('cache-control', CACHE_CONTROL)
|
|
112
|
+
.set(CORS_HEADERS)
|
|
113
|
+
.send('Login successful.');
|
|
114
|
+
return;
|
|
115
|
+
} finally {
|
|
116
|
+
delete this._loginState;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (this.loginError) {
|
|
121
|
+
res.status(400)
|
|
122
|
+
.set('cache-control', CACHE_CONTROL)
|
|
123
|
+
.send(this.loginError.message);
|
|
124
|
+
delete this.loginError;
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
res.status(302)
|
|
129
|
+
.set('cache-control', CACHE_CONTROL)
|
|
130
|
+
.set('location', '/')
|
|
131
|
+
.send('');
|
|
132
|
+
}
|
|
133
|
+
|
|
42
134
|
/**
|
|
43
135
|
* Proxy Mode route handler
|
|
44
136
|
* @param {Express.Request} req request
|
|
@@ -97,8 +189,8 @@ export class HelixServer extends BaseServer {
|
|
|
97
189
|
}
|
|
98
190
|
}
|
|
99
191
|
|
|
100
|
-
// use proxy
|
|
101
192
|
try {
|
|
193
|
+
// use proxy
|
|
102
194
|
const url = new URL(ctx.url, proxyUrl);
|
|
103
195
|
for (const [key, value] of proxyUrl.searchParams.entries()) {
|
|
104
196
|
url.searchParams.append(key, value);
|
|
@@ -111,6 +203,8 @@ export class HelixServer extends BaseServer {
|
|
|
111
203
|
cacheDirectory: this._project.cacheDirectory,
|
|
112
204
|
file404html: this._project.file404html,
|
|
113
205
|
siteToken: this._siteToken,
|
|
206
|
+
loginPath: LOGIN_ROUTE,
|
|
207
|
+
autoLogin: this._autoLogin,
|
|
114
208
|
});
|
|
115
209
|
} catch (err) {
|
|
116
210
|
log.error(`${pfx}failed to proxy AEM request ${ctx.path}: ${err.message}`);
|
|
@@ -126,6 +220,12 @@ export class HelixServer extends BaseServer {
|
|
|
126
220
|
this._liveReload = new LiveReload(this.log);
|
|
127
221
|
await this._liveReload.init(this.app, this._server);
|
|
128
222
|
}
|
|
223
|
+
|
|
224
|
+
this.app.get(LOGIN_ROUTE, asyncHandler(this.handleLogin.bind(this)));
|
|
225
|
+
this.app.get(LOGIN_ACK_ROUTE, asyncHandler(this.handleLoginAck.bind(this)));
|
|
226
|
+
this.app.post(LOGIN_ACK_ROUTE, express.json(), asyncHandler(this.handleLoginAck.bind(this)));
|
|
227
|
+
this.app.options(LOGIN_ACK_ROUTE, asyncHandler(this.handleLoginAck.bind(this)));
|
|
228
|
+
|
|
129
229
|
const handler = asyncHandler(this.handleProxyModeRequest.bind(this));
|
|
130
230
|
this.app.get('*', handler);
|
|
131
231
|
this.app.post('*', handler);
|
package/src/server/utils.js
CHANGED
|
@@ -355,11 +355,24 @@ window.LiveReloadOptions = {
|
|
|
355
355
|
.send(textBody);
|
|
356
356
|
return;
|
|
357
357
|
}
|
|
358
|
-
if (ret.status === 401) {
|
|
358
|
+
if (ret.status === 401 || ret.status === 403) {
|
|
359
|
+
const reqHeaders = req.headers;
|
|
360
|
+
if (opts.autoLogin && opts.loginPath
|
|
361
|
+
&& reqHeaders?.['sec-fetch-dest'] === 'document'
|
|
362
|
+
&& reqHeaders?.['sec-fetch-mode'] === 'navigate'
|
|
363
|
+
) {
|
|
364
|
+
// try to automatically login
|
|
365
|
+
res.set('location', opts.loginPath).status(302).send();
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
359
369
|
let textBody = await ret.text();
|
|
360
370
|
textBody = `<html>
|
|
361
371
|
<head><meta property="hlx:proxyUrl" content="${url}"></head>
|
|
362
|
-
<body
|
|
372
|
+
<body>
|
|
373
|
+
<pre>${textBody}</pre>
|
|
374
|
+
<p>Click <b><a href="${opts.loginPath}">here</a></b> to login.</p>
|
|
375
|
+
</body>
|
|
363
376
|
</html>
|
|
364
377
|
`;
|
|
365
378
|
respHeaders['content-type'] = 'text/html';
|
package/src/up.cmd.js
CHANGED
|
@@ -99,6 +99,17 @@ export default class UpCommand extends AbstractServerCommand {
|
|
|
99
99
|
.replace(/\{\{repo\}\}/, this._gitUrl.repo);
|
|
100
100
|
}
|
|
101
101
|
this._project.withProxyUrl(this._url);
|
|
102
|
+
const { site, org } = this.extractSiteAndOrg(this._url);
|
|
103
|
+
if (site && org) {
|
|
104
|
+
this._project
|
|
105
|
+
.withSite(site)
|
|
106
|
+
.withOrg(org)
|
|
107
|
+
.withSiteLoginUrl(
|
|
108
|
+
// TODO switch to production URL
|
|
109
|
+
`https://admin.hlx.page/login/${org}/${site}/main?client_id=aem-cli&redirect_uri=${encodeURIComponent(`http://localhost:${this._httpPort}/.aem/cli/login/ack`)}`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
102
113
|
await this.initServerOptions();
|
|
103
114
|
|
|
104
115
|
try {
|
|
@@ -113,6 +124,21 @@ export default class UpCommand extends AbstractServerCommand {
|
|
|
113
124
|
});
|
|
114
125
|
}
|
|
115
126
|
|
|
127
|
+
// eslint-disable-next-line class-methods-use-this
|
|
128
|
+
extractSiteAndOrg(url) {
|
|
129
|
+
const { hostname } = new URL(url);
|
|
130
|
+
const parts = hostname.split('.');
|
|
131
|
+
const errorResult = { site: null, org: null };
|
|
132
|
+
if (parts.length < 3) {
|
|
133
|
+
return errorResult;
|
|
134
|
+
}
|
|
135
|
+
if (!['live', 'page'].includes(parts[2]) || !['hlx', 'aem'].includes(parts[1])) {
|
|
136
|
+
return errorResult;
|
|
137
|
+
}
|
|
138
|
+
const [, site, org] = parts[0].split('--');
|
|
139
|
+
return { site, org };
|
|
140
|
+
}
|
|
141
|
+
|
|
116
142
|
async verifyUrl(gitUrl, ref) {
|
|
117
143
|
// check if the site is on helix5
|
|
118
144
|
// https://admin.hlx.page/sidekick/adobe/www-aem-live/main/config.json
|
package/src/up.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import path from 'path';
|
|
13
13
|
import { getOrCreateLogger } from './log-common.js';
|
|
14
|
+
import { getSiteTokenFromFile } from './config/config-utils.js';
|
|
14
15
|
|
|
15
16
|
export default function up() {
|
|
16
17
|
let executor;
|
|
@@ -121,7 +122,7 @@ export default function up() {
|
|
|
121
122
|
.withOpen(path.basename(argv.$0) === 'aem' ? argv.open : false)
|
|
122
123
|
.withTLS(argv.tlsKey, argv.tlsCert)
|
|
123
124
|
.withLiveReload(argv.livereload)
|
|
124
|
-
.withSiteToken(argv.siteToken)
|
|
125
|
+
.withSiteToken(argv.siteToken || await getSiteTokenFromFile())
|
|
125
126
|
.withUrl(argv.url)
|
|
126
127
|
.withPrintIndex(argv.printIndex)
|
|
127
128
|
.withAllowInsecure(argv.allowInsecure)
|