Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions nx/blocks/media-library/core/export.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { etcFetch } from './urls.js';
import { getMediaType, getSubtype } from './media.js';
import { decodeDisplayName, getFileName } from './files.js';
import { t } from './messages.js';
import { getMessage } from './messages.js';
import { isMediaLibraryPluginMode, withTimeout } from './utils.js';

function escapeCsvCell(value) {
Expand Down Expand Up @@ -187,8 +187,8 @@ export async function copyMediaToClipboard(media) {
try {
await copyNonImageLinkRichClipboard(media);
return {
heading: t('NOTIFY_PLUGIN_FALLBACK_HEADING'),
message: t('NOTIFY_PLUGIN_FALLBACK_LINK_MSG'),
heading: getMessage('NOTIFY_PLUGIN_FALLBACK_HEADING'),
message: getMessage('NOTIFY_PLUGIN_FALLBACK_LINK_MSG'),
};
} catch {
/* fall through to plain writeText */
Expand All @@ -202,23 +202,23 @@ export async function copyMediaToClipboard(media) {
await copyImageToClipboard(mediaUrl);
if (plugin) {
return {
heading: t('NOTIFY_PLUGIN_FALLBACK_HEADING'),
message: t('NOTIFY_PLUGIN_FALLBACK_IMAGE_MSG'),
heading: getMessage('NOTIFY_PLUGIN_FALLBACK_HEADING'),
message: getMessage('NOTIFY_PLUGIN_FALLBACK_IMAGE_MSG'),
};
}
return { heading: t('NOTIFY_COPIED'), message: t('NOTIFY_COPIED_IMAGE') };
return { heading: getMessage('NOTIFY_COPIED'), message: getMessage('NOTIFY_COPIED_IMAGE') };
}
await navigator.clipboard.writeText(mediaUrl);
if (plugin) {
return {
heading: t('NOTIFY_PLUGIN_FALLBACK_HEADING'),
message: t('NOTIFY_PLUGIN_FALLBACK_PLAIN_MSG'),
heading: getMessage('NOTIFY_PLUGIN_FALLBACK_HEADING'),
message: getMessage('NOTIFY_PLUGIN_FALLBACK_PLAIN_MSG'),
};
}
return { heading: t('NOTIFY_COPIED'), message: t('NOTIFY_COPIED_URL') };
return { heading: getMessage('NOTIFY_COPIED'), message: getMessage('NOTIFY_COPIED_URL') };
} catch (error) {
// eslint-disable-next-line no-console
console.error('Failed to copy to clipboard:', error);
return { heading: t('NOTIFY_ERROR'), message: t('NOTIFY_COPY_ERROR') };
return { heading: getMessage('NOTIFY_ERROR'), message: getMessage('NOTIFY_COPY_ERROR') };
}
}
22 changes: 11 additions & 11 deletions nx/blocks/media-library/core/indexing-adapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

import { updateAppState, getAppState, showNotification } from './state.js';
import { t } from './messages.js';
import { getMessage } from './messages.js';
import { clearProcessDataCache } from '../ui/filters.js';
import { getDedupeKey } from './urls.js';
import { getCanonicalMediaTimestamp } from './utils.js';
Expand Down Expand Up @@ -82,22 +82,22 @@ function getErrorMessage(errorCode, technicalMessage) {
switch (errorCode) {
case IndexingErrorCode.AUTH_REQUIRED:
case IndexingErrorCode.AUTH_FAILED:
return t('NOTIFY_SIGN_IN');
return getMessage('NOTIFY_SIGN_IN');
case IndexingErrorCode.DA_READ_DENIED:
return t('DA_READ_DENIED');
return getMessage('DA_READ_DENIED');
case IndexingErrorCode.DA_WRITE_DENIED:
case IndexingErrorCode.DA_SAVE_FAILED:
return t('DA_SAVE_FAILED');
return getMessage('DA_SAVE_FAILED');
case IndexingErrorCode.INDEX_PARSE_ERROR:
return t('INDEX_PARSE_ERROR');
return getMessage('INDEX_PARSE_ERROR');
case IndexingErrorCode.LOCK_CREATE_FAILED:
return t('LOCK_CREATE_FAILED_GENERIC');
return getMessage('LOCK_CREATE_FAILED_GENERIC');
case IndexingErrorCode.LOCK_REMOVE_FAILED:
return t('LOCK_REMOVE_FAILED');
return getMessage('LOCK_REMOVE_FAILED');
case IndexingErrorCode.NETWORK_TIMEOUT:
case IndexingErrorCode.BUILD_FAILED:
default:
return technicalMessage || t('NOTIFY_DISCOVERY_FAILED');
return technicalMessage || getMessage('NOTIFY_DISCOVERY_FAILED');
}
}

Expand Down Expand Up @@ -147,7 +147,7 @@ export function handleIndexingEvent(event, onMediaDataUpdated) {
clearProcessDataCache();

if (event.lockRemoveFailed) {
showNotification(t('NOTIFY_WARNING'), t('LOCK_REMOVE_FAILED'), 'danger');
showNotification(getMessage('NOTIFY_WARNING'), getMessage('LOCK_REMOVE_FAILED'), 'danger');
}

if (event.hasChanges && event.data && event.data.length > 0) {
Expand Down Expand Up @@ -214,9 +214,9 @@ export function handleIndexingEvent(event, onMediaDataUpdated) {
// Show notification for non-persistent errors
if (!event.isPersistent && event.code !== IndexingErrorCode.AUTH_REQUIRED) {
const userMessage = getErrorMessage(event.code, event.message);
showNotification(t('NOTIFY_ERROR'), userMessage, 'danger');
showNotification(getMessage('NOTIFY_ERROR'), userMessage, 'danger');
} else if (event.code === IndexingErrorCode.AUTH_REQUIRED) {
showNotification(t('NOTIFY_ERROR'), t('NOTIFY_SIGN_IN'), 'danger');
showNotification(getMessage('NOTIFY_ERROR'), getMessage('NOTIFY_SIGN_IN'), 'danger');
}

resetProgressiveState();
Expand Down
9 changes: 7 additions & 2 deletions nx/blocks/media-library/core/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,16 @@ const MESSAGES = {
VALIDATION_PATH_EMPTY: 'Parent path not found or empty: {path}',
VALIDATION_SITE_PATH_FILE: 'Site path cannot point to a file',
VALIDATION_ENTER_SITE_URL: 'Enter a site URL to start. Example: https://main--site--org.aem.page',
VALIDATION_SERVER_ERROR: 'Server error occurred. Please try again or sign in if you\'re not authenticated.',
VALIDATION_NETWORK_ERROR: 'Network error. Check your connection and try again.',
DA_WRITE_DENIED: "Couldn't save your media results. You can still browse current results, but your changes weren't saved. Try again, refresh the page, or contact your admin for write access.",
DA_SAVE_FAILED: "Couldn't save your media results. You can still browse current results, but your changes weren't saved. Try again, refresh the page, or contact your admin.",
PARTIAL_SAVE: "Some save steps didn't complete. Media results may be incomplete or out of date. Try refreshing the page.",
EDS_LOG_DENIED: 'You need Author or higher permissions on EDS to see the latest media data. You can still browse existing media.',
EDS_LOG_DENIED: 'You need one of these roles in the EDS project: author, publish, or admin. Contact your project admin to add you to one of these roles. You can still browse existing media.',
EDS_LOG_DENIED_HINT: 'The author role (or higher: publish, admin) is required for log:read permission. Note: basic_author and basic_publish do NOT have log:read access.',
EDS_AUTH_EXPIRED: 'Session expired. Sign in again.',
PROTECTED_SITE_IMAGES_WARNING: 'Protected site - some images may not load, refresh or login into site in a new tab using sidekick',
DATA_LOAD_FAILED: 'Failed to load media data. Please ensure you are signed in.',

NOTIFY_SIGN_IN: 'Sign in to run discovery.',
NOTIFY_VERIFY_AUTH: 'Failed to verify authentication.',
Expand Down Expand Up @@ -89,7 +94,7 @@ const MESSAGES = {
UI_OPEN_IN_NEW_TAB: 'Open in new tab',
};

export function t(key, params = {}) {
export function getMessage(key, params = {}) {
const str = MESSAGES[key];
if (str == null) return key;

Expand Down
36 changes: 22 additions & 14 deletions nx/blocks/media-library/core/paths.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { daFetch } from '../../../utils/daFetch.js';
import { DA_ADMIN } from '../../../utils/utils.js';
import { Paths, Domains, DA_LIVE_EDIT_BASE } from './constants.js';
import { ErrorCodes, logMediaLibraryError } from './errors.js';
import { t } from './messages.js';
import { getMessage } from './messages.js';
import {
normalizeSitePath as _normalizeSitePath,
getContentPathFromSitePath as _getContentPathFromSitePath,
Expand Down Expand Up @@ -172,7 +172,7 @@ export function resolveAbsolutePath(path, isFolder = false) {
export async function validateSitePath(sitePath) {
if (!sitePath) {
logMediaLibraryError(ErrorCodes.VALIDATION_SITE_PATH_MISSING, {});
return { valid: false, error: t('VALIDATION_ENTER_SITE_URL') };
return { valid: false, error: getMessage('VALIDATION_ENTER_SITE_URL') };
}

const parts = sitePath.split('/').filter(Boolean);
Expand All @@ -181,7 +181,7 @@ export async function validateSitePath(sitePath) {
logMediaLibraryError(ErrorCodes.VALIDATION_SITE_PATH_MISSING, {});
return {
valid: false,
error: t('VALIDATION_ENTER_SITE_URL'),
error: getMessage('VALIDATION_ENTER_SITE_URL'),
};
}

Expand All @@ -200,7 +200,7 @@ export async function validateSitePath(sitePath) {
logMediaLibraryError(ErrorCodes.VALIDATION_SITE_NOT_FOUND, { path: `/${org}/${repo}`, status: 404 });
return {
valid: false,
error: t('VALIDATION_SITE_NOT_FOUND', { path: `/${org}/${repo}` }),
error: getMessage('VALIDATION_SITE_NOT_FOUND', { path: `/${org}/${repo}` }),
};
}

Expand All @@ -209,18 +209,22 @@ export async function validateSitePath(sitePath) {

if (resp.status === 404) {
logMediaLibraryError(ErrorCodes.VALIDATION_SITE_NOT_FOUND, { path: `/${org}/${repo}`, status: 404 });
return { valid: false, error: t('VALIDATION_SITE_NOT_FOUND', { path: `/${org}/${repo}` }) };
return { valid: false, error: getMessage('VALIDATION_SITE_NOT_FOUND', { path: `/${org}/${repo}` }) };
}

if (resp.status === 401 || resp.status === 403) {
logMediaLibraryError(ErrorCodes.DA_READ_DENIED, { path: `/${org}/${repo}`, status: resp.status });
return {
valid: false,
error: t('VALIDATION_SITE_403'),
suggestion: t('VALIDATION_SITE_403_SUGGESTION'),
error: getMessage('VALIDATION_SITE_403'),
suggestion: getMessage('VALIDATION_SITE_403_SUGGESTION'),
};
}

if (resp.status >= 500) {
return { valid: false, error: getMessage('VALIDATION_SERVER_ERROR') };
}

return { valid: false, error: `Validation failed: ${resp.status}` };
} catch (error) {
return { valid: false, error: error.message };
Expand All @@ -246,7 +250,7 @@ export async function validateSitePath(sitePath) {
});
return {
valid: false,
error: t('VALIDATION_PATH_NOT_FOUND', { path: parentPath }),
error: getMessage('VALIDATION_PATH_NOT_FOUND', { path: parentPath }),
};
}

Expand All @@ -257,11 +261,15 @@ export async function validateSitePath(sitePath) {
);
return {
valid: false,
error: t('VALIDATION_PATH_403'),
suggestion: t('VALIDATION_PATH_403_SUGGESTION'),
error: getMessage('VALIDATION_PATH_403'),
suggestion: getMessage('VALIDATION_PATH_403_SUGGESTION'),
};
}

if (resp.status >= 500) {
return { valid: false, error: getMessage('VALIDATION_SERVER_ERROR') };
}

return { valid: false, error: `Validation failed: ${resp.status}` };
}

Expand All @@ -271,7 +279,7 @@ export async function validateSitePath(sitePath) {
if (!items || items.length === 0) {
return {
valid: false,
error: t('VALIDATION_PATH_EMPTY', { path: parentPath }),
error: getMessage('VALIDATION_PATH_EMPTY', { path: parentPath }),
};
}

Expand All @@ -284,15 +292,15 @@ export async function validateSitePath(sitePath) {
logMediaLibraryError(ErrorCodes.VALIDATION_PATH_NOT_FOUND, { path: `${parentPath}/${lastSegment}`, status: 404 });
return {
valid: false,
error: t('VALIDATION_PATH_NOT_FOUND_CHILD', { path: `/${[org, repo, ...restPath].join('/')}` }),
suggestion: t('VALIDATION_PATH_NOT_FOUND_SUGGESTION', { segment: lastSegment, parentPath }),
error: getMessage('VALIDATION_PATH_NOT_FOUND_CHILD', { path: `/${[org, repo, ...restPath].join('/')}` }),
suggestion: getMessage('VALIDATION_PATH_NOT_FOUND_SUGGESTION', { segment: lastSegment, parentPath }),
};
}

if (targetEntry.ext) {
return {
valid: false,
error: t('VALIDATION_SITE_PATH_FILE'),
error: getMessage('VALIDATION_SITE_PATH_FILE'),
suggestion: parentPath,
isFile: true,
fileType: targetEntry.ext,
Expand Down
7 changes: 4 additions & 3 deletions nx/blocks/media-library/core/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,8 @@ export async function checkSiteAuthRequired(org, repo) {

try {
const response = await etcFetch(indexUrl, 'cors', { method: 'HEAD' });
const requiresAuth = response.status === 401 || response.status === 403;
const requiresAuth = response.status === 401
|| response.status === 403;
const result = { requiresAuth, status: response.status };

debugLog('Site auth check result', result);
Expand All @@ -145,8 +146,8 @@ export async function checkSiteAuthRequired(org, repo) {

export async function livePreviewLogin(owner, repo) {
try {
const { loadIms } = await import('../../../../nx2/utils/ims.js');
const { accessToken } = await loadIms() || {};
const { initIms } = await import('../../../utils/daFetch.js');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While this might work we should use NX2, so we can remove NX.

Also there is const { loadIms } = await import('../../utils/ims.js');util which is maybe used better here https://github.com/adobe/da-nx/blob/main/nx/utils/ims.js

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're using nx here instead of nx2 because nx provides the setImsDetails(token) function in daFetch.js which enables token injection in plugin mode. This capability is critical for the SDK integration where tokens need to be injected externally.

Currently, nx2 doesn't have this token injection capability. Should we enhance nx2 to support this use case so we can eventually migrate away from nx?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure I fully understand the flow and usage of setImsDetails(...) are you doing a different IMS login?

Also would like to get a thought from @andreituicu on this from security pov.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question! Here's the flow:

Current Implementation (using nx)

The media library supports plugin mode where it runs embedded in another application (like Adobe Express or other Adobe tools). In this mode:

  1. Host app handles authentication: The parent application already has an authenticated IMS session
  2. Token is passed via MessagePort: The parent sends the IMS token through postMessage when initializing the SDK (see nx/utils/sdk.js:70-71)
  3. Token is injected: setImsDetails(token) stores this externally-provided token
  4. No duplicate auth: The media library reuses the host's token instead of triggering its own IMS login

Code flow:

// In host app
mediaLibraryIframe.contentWindow.postMessage({ ready: true, token }, '*', [messagePort])

// In media library (nx/utils/sdk.js:70-71)
if (e.data.token) {
  setImsDetails(e.data.token);  // Inject external token
}

// Later in daFetch (nx/utils/daFetch.js:22-24)
if (imsDetails) {  // Use injected token
  const { accessToken } = await initIms();
  opts.headers.Authorization = `Bearer ${accessToken.token}`;
}

nx2 Limitation

nx2/utils/ims.js always calls loadIms() which:

  • Loads the Adobe IMS library directly
  • Performs its own authentication flow
  • Has no mechanism to accept externally-provided tokens

Security Note

This is not a "different IMS login" - it's reusing an already-authenticated token from a trusted parent application. The host app has already completed Adobe IMS authentication; we're just accepting that token rather than forcing a second auth.

@andreituicu - Would appreciate your security review on this approach. The alternative would be to add setImsDetails() capability to nx2.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 For the use case of the embedding in non-DA properties I think this should either live somewhere else or in some Media Library only code, not in a shared one.

if we need special handling for IMS here for this use case this not be part of NX or DA core.

@kmurugulla kmurugulla Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in this specific use case , the media library code from da-nx is being loaded via da.live in the plugin mode. Plugin being used in layout / da editor mode. Even though plugin code is part of non-da.live related repo, as the plugin is loaded on da.live , i thought i would reuse as much as possible from da-nx.

I am open to move the specific IMS functionalities that will enable me to reuse code between app and plugin into media library , however it will sort of creates duplication of code. And i don't know whether there are any other apps that needed this model of reuse app and plugin from same code base.

Plugin reference - https://github.com/adobe-rnd/aem-apps/blob/main/tools/plugins/media-library/media-library.html

@mhaack Since I have couple of other changes added into this PR , if it is agreeable , i can take up the IMS related functionality ( moving into media library) in another PR

const { accessToken } = await initIms() || {};
const url = `${getLivePreviewUrl(owner, repo)}/gimme_cookie`;

debugLog('Setting preview.da.live cookie', { owner, repo, url });
Expand Down
28 changes: 20 additions & 8 deletions nx/blocks/media-library/indexing/admin-api.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
} from '../core/constants.js';
import { MediaLibraryError, ErrorCodes, logMediaLibraryError } from '../core/errors.js';
import { isPerfEnabled } from '../core/params.js';
import { t } from '../core/messages.js';
import { getMessage } from '../core/messages.js';

const AEM_PAGE_MARKDOWN_RATE = 180; /* keep some headroom under the 200 RPS host limit */
const AEM_SITE_AUTH_DENIED = new Set([401, 403]);
Expand Down Expand Up @@ -368,22 +368,34 @@ export async function streamLog(

if (!resp.ok) {
if (resp.status === 403) {
logMediaLibraryError(ErrorCodes.EDS_LOG_DENIED, { status: 403, endpoint: nextUrl });
throw new MediaLibraryError(
logMediaLibraryError(
ErrorCodes.EDS_LOG_DENIED,
t('EDS_LOG_DENIED'),
{ status: 403, endpoint: nextUrl },
);
const err = new MediaLibraryError(
ErrorCodes.EDS_LOG_DENIED,
getMessage('EDS_LOG_DENIED'),
{ status: 403, endpoint: nextUrl, hint: getMessage('EDS_LOG_DENIED_HINT') },
);
err.status = 403;
throw err;
}
if (resp.status === 401) {
logMediaLibraryError(ErrorCodes.EDS_AUTH_EXPIRED, { status: 401, endpoint: nextUrl });
throw new MediaLibraryError(
logMediaLibraryError(
ErrorCodes.EDS_AUTH_EXPIRED,
t('EDS_AUTH_EXPIRED'),
{ status: 401, endpoint: nextUrl },
);
const err = new MediaLibraryError(
ErrorCodes.EDS_AUTH_EXPIRED,
getMessage('EDS_AUTH_EXPIRED'),
{ status: 401, endpoint: nextUrl },
);
err.status = 401;
throw err;
}
throw new Error(`${endpoint} API error: ${resp.status} ${resp.statusText}`);
const err = new Error(`${endpoint} API error: ${resp.status} ${resp.statusText}`);
err.status = resp.status;
throw err;
}

const data = await resp.json();
Expand Down
14 changes: 10 additions & 4 deletions nx/blocks/media-library/indexing/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,21 +79,24 @@ async function runWorkerBuild(
// When running with ?nx=local, files load from localhost but page is on da.live
// Workers must be same-origin, so we create a blob URL
const workerUrl = new URL('./worker/worker.js', import.meta.url).href;
const response = await fetch(workerUrl);
// Add cache-busting to ensure fresh worker code is loaded
const cacheBustedUrl = `${workerUrl}?t=${Date.now()}`;
const response = await fetch(cacheBustedUrl, { cache: 'no-store' });
if (!response.ok) {
throw new Error(`Failed to fetch worker code: ${response.status}`);
}

let workerCode = await response.text();

// Replace ALL relative imports with absolute URLs so worker can fetch them
// This converts: import './foo.js' → import 'http://localhost:6456/.../foo.js'
// This converts: import './foo.js' → import 'http://localhost:6456/.../foo.js?t=...'
const baseUrl = new URL('./worker/', import.meta.url).href;
const cacheBuster = Date.now();
workerCode = workerCode.replace(
/from\s+['"](\.\.[^'"]*|\.\/[^'"]*)['"]/g,
(match, path) => {
const absoluteUrl = new URL(path, baseUrl).href;
return `from '${absoluteUrl}'`;
return `from '${absoluteUrl}?t=${cacheBuster}'`;
},
);

Expand Down Expand Up @@ -154,7 +157,10 @@ async function runWorkerBuild(
resolve(data);
} else if (type === 'error') {
clearTimeout(watchdogTimer);
reject(new Error(error.message || 'Worker error'));
const err = new Error(error.message || 'Worker error');
err.code = error.code;
err.status = error.status;
reject(err);
}
};

Expand Down
2 changes: 1 addition & 1 deletion nx/blocks/media-library/indexing/linked-content.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { fetchFileGetInfo, fetchFileHeadInfo } from './admin-api.js';
import { Operation } from '../core/constants.js';
import {
isPdf,
isSvg,
Expand All @@ -10,7 +11,6 @@ import {
toExternalMediaEntry,
} from './parse.js';
import { normalizePath } from './parse-utils.js';
import { Operation } from '../core/constants.js';
import {
isIndexedExternalMediaOperation,
isIndexedExternalMediaEntry,
Expand Down
Loading
Loading