@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 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.8.6",
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.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.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.4",
75
+ "uuid": "11.0.5",
75
76
  "yargs": "17.7.2"
76
77
  },
77
78
  "devDependencies": {
package/src/cli.js CHANGED
@@ -121,7 +121,7 @@ export default class CLI {
121
121
  Object.values(this._commands)
122
122
  .forEach((cmd) => argv.command(cmd));
123
123
 
124
- logArgs(argv)
124
+ await logArgs(argv)
125
125
  .strictCommands(true)
126
126
  .scriptName('aem')
127
127
  .usage('Usage: $0 <command> [options]')
@@ -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
  /**
@@ -125,6 +125,10 @@ export default class HeadHtmlSupport {
125
125
  }
126
126
  }
127
127
 
128
+ setSiteToken(siteToken) {
129
+ this.siteToken = siteToken;
130
+ }
131
+
128
132
  invalidateLocal() {
129
133
  this.localStatus = 0;
130
134
  }
@@ -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);
@@ -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><pre>${textBody}</pre></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)