@adobe/aem-cli 16.20.8 → 16.20.10
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.20.10](https://github.com/adobe/helix-cli/compare/v16.20.9...v16.20.10) (2026-07-08)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **server:** auth to da.live preview host for content.da.live images ([#2754](https://github.com/adobe/helix-cli/issues/2754)) ([c3e4b53](https://github.com/adobe/helix-cli/commit/c3e4b53c2d8a8844a7917e3aff0c856ea7d1c631)), closes [#2752](https://github.com/adobe/helix-cli/issues/2752)
|
|
7
|
+
|
|
8
|
+
## [16.20.9](https://github.com/adobe/helix-cli/compare/v16.20.8...v16.20.9) (2026-07-07)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* inject hlx:proxyUrl meta for pages served from local content/ ([#2753](https://github.com/adobe/helix-cli/issues/2753)) ([271bfcf](https://github.com/adobe/helix-cli/commit/271bfcfa01316944ce5b4e26e767d59a189b78bc))
|
|
14
|
+
|
|
1
15
|
## [16.20.8](https://github.com/adobe/helix-cli/compare/v16.20.7...v16.20.8) (2026-07-06)
|
|
2
16
|
|
|
3
17
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adobe/aem-cli",
|
|
3
|
-
"version": "16.20.
|
|
3
|
+
"version": "16.20.10",
|
|
4
4
|
"description": "AEM CLI",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -58,6 +58,7 @@
|
|
|
58
58
|
"diff": "9.0.0",
|
|
59
59
|
"dotenv": "17.4.2",
|
|
60
60
|
"express": "5.2.1",
|
|
61
|
+
"express-rate-limit": "8.5.2",
|
|
61
62
|
"faye-websocket": "0.11.4",
|
|
62
63
|
"fs-extra": "11.3.5",
|
|
63
64
|
"glob": "13.0.6",
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* da.live content-image auth bootstrap.
|
|
15
|
+
* Only injected into pages that reference a *.preview.da.live host, since that host
|
|
16
|
+
* requires an IMS-authenticated cookie. First checks whether the browser already has
|
|
17
|
+
* a valid (non-expired) cookie for that host; only if not does it load IMS and either
|
|
18
|
+
* forward the access token to /gimme_cookie, or send the browser through the CLI's own
|
|
19
|
+
* /.aem/cli/da-login redirect (imslib's signIn() ignores this page's origin as the
|
|
20
|
+
* redirect_uri — the darkalley IMS client only allows its fixed :9898 callback).
|
|
21
|
+
* This script is self-contained and browser-compatible (no module system).
|
|
22
|
+
*/
|
|
23
|
+
(function iife() {
|
|
24
|
+
var cfg = window.DaContentAuthConfig;
|
|
25
|
+
if (!cfg || !cfg.previewOrigin || !cfg.clientId) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// sendCookie() only ever runs after we've determined the browser had no valid
|
|
30
|
+
// preview cookie, meaning any preview-host images on this page already fired
|
|
31
|
+
// (and failed) before the cookie existed. Reload once so they're refetched with
|
|
32
|
+
// the new cookie attached — without this, the page looks broken until the user
|
|
33
|
+
// manually reloads.
|
|
34
|
+
function sendCookie(token) {
|
|
35
|
+
window.fetch(`${cfg.previewOrigin}/gimme_cookie`, {
|
|
36
|
+
method: 'GET',
|
|
37
|
+
credentials: 'include',
|
|
38
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
39
|
+
}).then(function onResponse(res) {
|
|
40
|
+
if (res.ok) {
|
|
41
|
+
window.location.reload();
|
|
42
|
+
}
|
|
43
|
+
}).catch(function onError() {
|
|
44
|
+
// non-fatal: images will fail to load, page still renders
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function redirectToLogin() {
|
|
49
|
+
var returnUrl = window.location.href.split('#')[0];
|
|
50
|
+
window.location.href = `/.aem/cli/da-login?return=${encodeURIComponent(returnUrl)}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function bootstrapIms() {
|
|
54
|
+
window.adobeid = {
|
|
55
|
+
client_id: cfg.clientId,
|
|
56
|
+
scope: cfg.scope,
|
|
57
|
+
environment: 'prod',
|
|
58
|
+
autoValidateToken: true,
|
|
59
|
+
useLocalStorage: true,
|
|
60
|
+
onReady: function onReady() {
|
|
61
|
+
var accessToken = window.adobeIMS.getAccessToken();
|
|
62
|
+
if (accessToken) {
|
|
63
|
+
sendCookie(accessToken.token);
|
|
64
|
+
} else {
|
|
65
|
+
redirectToLogin();
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
onError: function onError() {
|
|
69
|
+
// non-fatal: images will fail to load, page still renders
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
var script = document.createElement('script');
|
|
73
|
+
script.src = 'https://auth.services.adobe.com/imslib/imslib.min.js';
|
|
74
|
+
document.head.appendChild(script);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// A credentialed request to an actual gated asset fails (401/403) whenever there's
|
|
78
|
+
// no cookie yet or the existing one has expired — the server re-checks it every
|
|
79
|
+
// request, so this alone covers both cases without us tracking expiry ourselves.
|
|
80
|
+
// Probing the site root wouldn't work: it's often served regardless of auth, only
|
|
81
|
+
// the assets themselves are gated.
|
|
82
|
+
function hasValidCookie() {
|
|
83
|
+
return window.fetch(`${cfg.previewOrigin}${cfg.probePath || '/'}`, {
|
|
84
|
+
method: 'HEAD',
|
|
85
|
+
credentials: 'include',
|
|
86
|
+
cache: 'no-store',
|
|
87
|
+
}).then(function onResponse(res) {
|
|
88
|
+
return res.ok;
|
|
89
|
+
}).catch(function onError() {
|
|
90
|
+
return false;
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Returning from the /.aem/cli/da-login round trip: the access token is in the URL fragment.
|
|
95
|
+
var hashParams = new URLSearchParams(window.location.hash.replace(/^#/, ''));
|
|
96
|
+
var tokenFromRedirect = hashParams.get('access_token');
|
|
97
|
+
if (tokenFromRedirect) {
|
|
98
|
+
window.history.replaceState(null, '', window.location.pathname + window.location.search);
|
|
99
|
+
sendCookie(tokenFromRedirect);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
hasValidCookie().then(function onChecked(valid) {
|
|
104
|
+
if (!valid) {
|
|
105
|
+
bootstrapIms();
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
}());
|
package/src/content/da-auth.js
CHANGED
|
@@ -16,8 +16,11 @@ import open from 'open';
|
|
|
16
16
|
import { ensureGitIgnored } from './content-git.js';
|
|
17
17
|
|
|
18
18
|
const IMS_ORIGIN = 'https://ims-na1.adobelogin.com';
|
|
19
|
-
|
|
20
|
-
const
|
|
19
|
+
/** Shared with da-live's own IMS client (see da-live/scripts/scripts.js). */
|
|
20
|
+
export const DA_IMS_CLIENT_ID = 'darkalley';
|
|
21
|
+
export const DA_IMS_SCOPE = 'ab.manage,AdobeID,gnav,openid,org.read,read_organizations,session,aem.frontend.all,additional_info.ownerOrg,additional_info.projectedProductContext,account_cluster.read';
|
|
22
|
+
const CLIENT_ID = DA_IMS_CLIENT_ID;
|
|
23
|
+
const SCOPE = DA_IMS_SCOPE;
|
|
21
24
|
const CALLBACK_PORT = 9898;
|
|
22
25
|
const REDIRECT_URI = `http://localhost:${CALLBACK_PORT}/callback`;
|
|
23
26
|
|
|
@@ -70,11 +73,15 @@ function isTokenExpired(stored) {
|
|
|
70
73
|
* IMS redirects to http://localhost:{port}/callback#access_token=TOKEN
|
|
71
74
|
* The fragment never reaches the server, so /callback serves a tiny HTML page
|
|
72
75
|
* that reads the fragment via JS and forwards the token to /token, then
|
|
73
|
-
* redirects the browser to
|
|
76
|
+
* redirects the browser on success — to `finalRedirectUrl` (with the token appended
|
|
77
|
+
* as a URL fragment) when given, otherwise to https://tools.aem.live/cli/logged-in.
|
|
74
78
|
*
|
|
79
|
+
* @param {string} [finalRedirectUrl] where to send the browser after login,
|
|
80
|
+
* used by the `aem up` dev-server login flow to return to the page that
|
|
81
|
+
* triggered it. Omitted for the CLI's own `content clone`/`content push` login.
|
|
75
82
|
* @returns {Promise<{token: string, expiresIn: number|null}>}
|
|
76
83
|
*/
|
|
77
|
-
function waitForToken() {
|
|
84
|
+
function waitForToken(finalRedirectUrl) {
|
|
78
85
|
return new Promise((resolve, reject) => {
|
|
79
86
|
let timeout;
|
|
80
87
|
const server = http.createServer((req, res) => {
|
|
@@ -83,6 +90,9 @@ function waitForToken() {
|
|
|
83
90
|
// Step 1: IMS lands here with the token in the fragment.
|
|
84
91
|
// Serve a page that extracts it and calls /token.
|
|
85
92
|
if (url.pathname === '/callback') {
|
|
93
|
+
const finalizeJs = finalRedirectUrl
|
|
94
|
+
? "window.location.href = loggedInUrl + '#access_token=' + encodeURIComponent(token) + (expiresIn ? '&expires_in=' + encodeURIComponent(expiresIn) : '');"
|
|
95
|
+
: 'window.location.href = loggedInUrl;';
|
|
86
96
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
87
97
|
res.end(`<!DOCTYPE html><html><head><title>Logging in...</title></head><body>
|
|
88
98
|
<script>
|
|
@@ -93,7 +103,7 @@ function waitForToken() {
|
|
|
93
103
|
const dest = token
|
|
94
104
|
? '/token?access_token=' + encodeURIComponent(token) + (expiresIn ? '&expires_in=' + encodeURIComponent(expiresIn) : '')
|
|
95
105
|
: '/token?error=' + encodeURIComponent(error || 'unknown');
|
|
96
|
-
const loggedInUrl = 'https://tools.aem.live/cli/logged-in';
|
|
106
|
+
const loggedInUrl = ${JSON.stringify(finalRedirectUrl || 'https://tools.aem.live/cli/logged-in')};
|
|
97
107
|
if (!token) {
|
|
98
108
|
fetch(dest);
|
|
99
109
|
document.body.innerHTML = '<h2>Login failed.</h2>';
|
|
@@ -102,7 +112,7 @@ function waitForToken() {
|
|
|
102
112
|
document.body.appendChild(errP);
|
|
103
113
|
} else {
|
|
104
114
|
fetch(dest)
|
|
105
|
-
.then(() => {
|
|
115
|
+
.then(() => { ${finalizeJs} })
|
|
106
116
|
.catch(() => {
|
|
107
117
|
document.body.innerHTML = '<h2>Login failed.</h2><p>Could not complete login.</p>';
|
|
108
118
|
});
|
|
@@ -176,6 +186,31 @@ async function login(log, projectDir) {
|
|
|
176
186
|
|
|
177
187
|
// ─── Public API ──────────────────────────────────────────────────────────────
|
|
178
188
|
|
|
189
|
+
/**
|
|
190
|
+
* Starts the IMS login flow for the `aem up` dev server: unlike {@link getValidToken},
|
|
191
|
+
* this doesn't open a browser itself — the caller already has one open (the page that
|
|
192
|
+
* needs auth). Returns the IMS authorize URL to redirect that page to; the browser
|
|
193
|
+
* comes back to `finalRedirectUrl` with `#access_token=...` once login completes.
|
|
194
|
+
*
|
|
195
|
+
* The `darkalley` IMS client only allows `http://localhost:9898/callback` as a
|
|
196
|
+
* redirect_uri (arbitrary localhost ports/paths are rejected), so the round trip
|
|
197
|
+
* always passes through the fixed callback server before returning to the caller.
|
|
198
|
+
*
|
|
199
|
+
* @param {string} finalRedirectUrl page to send the browser back to after login
|
|
200
|
+
* @returns {string} the IMS authorize URL to redirect the browser to
|
|
201
|
+
*/
|
|
202
|
+
export function startDaLoginRedirect(finalRedirectUrl) {
|
|
203
|
+
const params = new URLSearchParams({
|
|
204
|
+
response_type: 'token',
|
|
205
|
+
client_id: CLIENT_ID,
|
|
206
|
+
scope: SCOPE,
|
|
207
|
+
redirect_uri: REDIRECT_URI,
|
|
208
|
+
});
|
|
209
|
+
// fire-and-forget: the callback server delivers the browser to finalRedirectUrl itself
|
|
210
|
+
waitForToken(finalRedirectUrl).catch(() => {});
|
|
211
|
+
return `${IMS_ORIGIN}/ims/authorize/v2?${params}`;
|
|
212
|
+
}
|
|
213
|
+
|
|
179
214
|
/**
|
|
180
215
|
* Returns a valid da.live access token. Triggers browser login if needed.
|
|
181
216
|
*
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import crypto from 'crypto';
|
|
13
13
|
import express from 'express';
|
|
14
|
+
import { rateLimit } from 'express-rate-limit';
|
|
14
15
|
import { promisify } from 'util';
|
|
15
16
|
import path from 'path';
|
|
16
17
|
import { lstat, readFile } from 'fs/promises';
|
|
@@ -22,9 +23,20 @@ import LiveReload from './LiveReload.js';
|
|
|
22
23
|
import { saveSiteTokenToFile } from '../config/config-utils.js';
|
|
23
24
|
import { CONTENT_DIR } from '../content/content-shared.js';
|
|
24
25
|
import { transformContentMetadataHtml } from '../content/content-metadata-html.js';
|
|
26
|
+
import { DA_IMS_CLIENT_ID, DA_IMS_SCOPE, startDaLoginRedirect } from '../content/da-auth.js';
|
|
25
27
|
|
|
26
28
|
const LOGIN_ROUTE = '/.aem/cli/login';
|
|
27
29
|
const LOGIN_ACK_ROUTE = '/.aem/cli/login/ack';
|
|
30
|
+
const DA_LOGIN_ROUTE = '/.aem/cli/da-login';
|
|
31
|
+
|
|
32
|
+
// Local dev-server only, but both routes trigger real side effects (a live server
|
|
33
|
+
// bind on :9898, an outbound redirect to IMS) — cap abuse from a runaway page/script.
|
|
34
|
+
const daContentAuthRateLimit = rateLimit({
|
|
35
|
+
windowMs: 60_000,
|
|
36
|
+
limit: 20,
|
|
37
|
+
standardHeaders: true,
|
|
38
|
+
legacyHeaders: false,
|
|
39
|
+
});
|
|
28
40
|
|
|
29
41
|
// HTML folder candidate extensions, in lookup order.
|
|
30
42
|
// First entry takes precedence when multiple candidates exist on disk.
|
|
@@ -140,6 +152,34 @@ export class HelixServer extends BaseServer {
|
|
|
140
152
|
res.status(302).set('location', loginUrl).send('');
|
|
141
153
|
}
|
|
142
154
|
|
|
155
|
+
/**
|
|
156
|
+
* Kicks off the IMS login flow for a page that needs the da.live preview cookie
|
|
157
|
+
* (see {@link utils.injectDaContentAuthScript}). Redirects to IMS; the browser comes
|
|
158
|
+
* back to the `return` url (validated same-origin, to avoid leaking the token via an
|
|
159
|
+
* open redirect) with `#access_token=...` once the fixed :9898 callback catches it.
|
|
160
|
+
*/
|
|
161
|
+
handleDaLogin(req, res) {
|
|
162
|
+
if (!req.query.return) {
|
|
163
|
+
res.status(400).send('Invalid or missing return url.');
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const expectedOrigin = `${req.protocol}://${req.get('host')}`;
|
|
167
|
+
let target;
|
|
168
|
+
try {
|
|
169
|
+
const parsed = new URL(req.query.return, expectedOrigin);
|
|
170
|
+
if (parsed.origin !== expectedOrigin) {
|
|
171
|
+
throw new Error('cross-origin return url');
|
|
172
|
+
}
|
|
173
|
+
target = parsed.href;
|
|
174
|
+
} catch (e) {
|
|
175
|
+
res.status(400).send('Invalid or missing return url.');
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
this.log.debug(`Starting da.live login, returning to ${target} when done.`);
|
|
179
|
+
const authUrl = startDaLoginRedirect(target);
|
|
180
|
+
res.status(302).set('location', authUrl).send('');
|
|
181
|
+
}
|
|
182
|
+
|
|
143
183
|
async handleLoginAck(req, res) {
|
|
144
184
|
const CACHE_CONTROL = 'no-store, private, must-revalidate';
|
|
145
185
|
const CORS_HEADERS = {
|
|
@@ -466,6 +506,17 @@ export class HelixServer extends BaseServer {
|
|
|
466
506
|
? `${contentFilePath.slice(0, -'.plain.html'.length)}.html`
|
|
467
507
|
: contentFilePath;
|
|
468
508
|
let htmlContent = await readFile(servedFilePath, 'utf-8');
|
|
509
|
+
htmlContent = utils.rewriteDaContentImageUrls(
|
|
510
|
+
htmlContent,
|
|
511
|
+
this._project.org,
|
|
512
|
+
this._project.site,
|
|
513
|
+
);
|
|
514
|
+
const previewOrigin = this._project.org && this._project.site
|
|
515
|
+
? `https://main--${this._project.site}--${this._project.org}.preview.da.live`
|
|
516
|
+
: null;
|
|
517
|
+
// Content may already reference the preview host directly (not just via
|
|
518
|
+
// the content.da.live rewrite above), so gate on presence, not on rewrite.
|
|
519
|
+
const needsDaContentAuth = !!previewOrigin && htmlContent.includes(previewOrigin);
|
|
469
520
|
if (isPlainFallback) {
|
|
470
521
|
if (liveReload) {
|
|
471
522
|
liveReload.registerFile(ctx.requestId, servedFilePath);
|
|
@@ -503,6 +554,21 @@ export class HelixServer extends BaseServer {
|
|
|
503
554
|
htmlContent = htmlContent.replace(/<\/head>/i, `${metaTagsHtml}</head>`);
|
|
504
555
|
}
|
|
505
556
|
}
|
|
557
|
+
const proxyPageUrl = new URL(ctx.url, proxyUrl);
|
|
558
|
+
for (const [key, value] of proxyUrl.searchParams.entries()) {
|
|
559
|
+
proxyPageUrl.searchParams.append(key, value);
|
|
560
|
+
}
|
|
561
|
+
htmlContent = utils.injectMeta(htmlContent, {
|
|
562
|
+
'hlx:proxyUrl': proxyPageUrl.href,
|
|
563
|
+
});
|
|
564
|
+
if (needsDaContentAuth) {
|
|
565
|
+
htmlContent = utils.injectDaContentAuthScript(htmlContent, {
|
|
566
|
+
previewOrigin,
|
|
567
|
+
probePath: utils.findDaPreviewProbePath(htmlContent, previewOrigin),
|
|
568
|
+
clientId: DA_IMS_CLIENT_ID,
|
|
569
|
+
scope: DA_IMS_SCOPE,
|
|
570
|
+
});
|
|
571
|
+
}
|
|
506
572
|
if (liveReload) {
|
|
507
573
|
htmlContent = utils.injectLiveReloadScript(htmlContent, this);
|
|
508
574
|
liveReload.registerFile(ctx.requestId, contentFilePath);
|
|
@@ -606,6 +672,12 @@ export class HelixServer extends BaseServer {
|
|
|
606
672
|
this.app.get(LOGIN_ACK_ROUTE, asyncHandler(this.handleLoginAck.bind(this)));
|
|
607
673
|
this.app.post(LOGIN_ACK_ROUTE, express.json(), asyncHandler(this.handleLoginAck.bind(this)));
|
|
608
674
|
this.app.options(LOGIN_ACK_ROUTE, asyncHandler(this.handleLoginAck.bind(this)));
|
|
675
|
+
this.app.get(
|
|
676
|
+
'/__internal__/da-content-auth.js',
|
|
677
|
+
daContentAuthRateLimit,
|
|
678
|
+
(req, res) => utils.serveDaContentAuthScript(res),
|
|
679
|
+
);
|
|
680
|
+
this.app.get(DA_LOGIN_ROUTE, daContentAuthRateLimit, this.handleDaLogin.bind(this));
|
|
609
681
|
|
|
610
682
|
// Add HTML folder handler before the general proxy handler
|
|
611
683
|
if (this._htmlFolder) {
|
package/src/server/utils.js
CHANGED
|
@@ -31,6 +31,10 @@ const CONSOLE_INTERCEPTOR = readFileSync(
|
|
|
31
31
|
path.join(__dirname, '../../packages/browser-injectables/src/console-interceptor.js'),
|
|
32
32
|
'utf-8',
|
|
33
33
|
);
|
|
34
|
+
const DA_CONTENT_AUTH_SCRIPT = readFileSync(
|
|
35
|
+
path.join(__dirname, '../../packages/browser-injectables/src/da-content-auth.js'),
|
|
36
|
+
'utf-8',
|
|
37
|
+
);
|
|
34
38
|
|
|
35
39
|
const utils = {
|
|
36
40
|
status2level(status, debug3xx) {
|
|
@@ -791,6 +795,76 @@ window.LiveReloadOptions = {
|
|
|
791
795
|
return `<html><head>${fullHead}</head><body><header></header><main>${content}</main><footer></footer></body></html>`;
|
|
792
796
|
},
|
|
793
797
|
|
|
798
|
+
/**
|
|
799
|
+
* Rewrites da.live content-store image URLs (`https://content.da.live/${org}/${site}/...`)
|
|
800
|
+
* to the site's preview domain, since content.da.live is not publicly reachable
|
|
801
|
+
* for rendering images during local dev.
|
|
802
|
+
* @param {string} html html content
|
|
803
|
+
* @param {string} org da.live org
|
|
804
|
+
* @param {string} site da.live site
|
|
805
|
+
* @returns {string} rewritten html
|
|
806
|
+
*/
|
|
807
|
+
rewriteDaContentImageUrls(html, org, site) {
|
|
808
|
+
if (!org || !site) {
|
|
809
|
+
return html;
|
|
810
|
+
}
|
|
811
|
+
const from = `https://content.da.live/${org}/${site}/`;
|
|
812
|
+
const to = `https://main--${site}--${org}.preview.da.live/`;
|
|
813
|
+
return html.split(from).join(to);
|
|
814
|
+
},
|
|
815
|
+
|
|
816
|
+
/**
|
|
817
|
+
* Finds one concrete asset path served from `previewOrigin` in the page (e.g. an
|
|
818
|
+
* image src), so the browser can probe *that* to check for a valid auth cookie —
|
|
819
|
+
* the site's root document is often served regardless of auth, only assets are
|
|
820
|
+
* gated, so probing `/` would give a false "already authorized" reading.
|
|
821
|
+
* @param {string} html html content
|
|
822
|
+
* @param {string} previewOrigin e.g. `https://main--site--org.preview.da.live`
|
|
823
|
+
* @returns {string} an absolute path (starting with `/`), defaults to `/` if none found
|
|
824
|
+
*/
|
|
825
|
+
findDaPreviewProbePath(html, previewOrigin) {
|
|
826
|
+
const escaped = previewOrigin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
827
|
+
const match = html.match(new RegExp(`${escaped}(/[^"'\\s)]*)`));
|
|
828
|
+
return match ? match[1] : '/';
|
|
829
|
+
},
|
|
830
|
+
|
|
831
|
+
/**
|
|
832
|
+
* Injects the IMS-auth bootstrap that forwards an access token to `/gimme_cookie`
|
|
833
|
+
* on the given preview origin, so images served from that origin (after
|
|
834
|
+
* {@link rewriteDaContentImageUrls}) are authorized. Redirects the browser to the
|
|
835
|
+
* IMS login screen if there is no existing session.
|
|
836
|
+
* @param {string} html full HTML document (must contain a `</head>`)
|
|
837
|
+
* @param {{ previewOrigin: string, probePath: string, clientId: string, scope: string }} options
|
|
838
|
+
* @returns {string} html with the auth bootstrap injected, unchanged if no `</head>` found
|
|
839
|
+
*/
|
|
840
|
+
injectDaContentAuthScript(html, {
|
|
841
|
+
previewOrigin, probePath, clientId, scope,
|
|
842
|
+
}) {
|
|
843
|
+
const match = html.match(/<\/head>/i);
|
|
844
|
+
if (!match) {
|
|
845
|
+
return html;
|
|
846
|
+
}
|
|
847
|
+
const { index } = match;
|
|
848
|
+
const nonceMatch = html.match(/nonce="([a-zA-Z0-9+/=]+)"/);
|
|
849
|
+
const nonce = nonceMatch ? ` nonce="${nonceMatch[1]}"` : '';
|
|
850
|
+
const config = JSON.stringify({
|
|
851
|
+
previewOrigin, probePath, clientId, scope,
|
|
852
|
+
});
|
|
853
|
+
const script = `<script${nonce}>window.DaContentAuthConfig=${config};</script>`
|
|
854
|
+
+ `<script${nonce} src="/__internal__/da-content-auth.js"></script>`;
|
|
855
|
+
return `${html.substring(0, index)}${script}${html.substring(index)}`;
|
|
856
|
+
},
|
|
857
|
+
|
|
858
|
+
/**
|
|
859
|
+
* Serves the da-content-auth bootstrap as a static JS file (referenced by
|
|
860
|
+
* {@link injectDaContentAuthScript}).
|
|
861
|
+
* @param {Express.Response} res response
|
|
862
|
+
*/
|
|
863
|
+
serveDaContentAuthScript(res) {
|
|
864
|
+
res.set('content-type', 'application/javascript');
|
|
865
|
+
res.send(DA_CONTENT_AUTH_SCRIPT);
|
|
866
|
+
},
|
|
867
|
+
|
|
794
868
|
/**
|
|
795
869
|
* Extracts the innerHTML of the <main> element from a full HTML document.
|
|
796
870
|
* Returns the original content unchanged if no <main> is found.
|