Skip to content
Closed
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ node_modules
dist
coverage
.project
.vscode
build
package-lock.json
1 change: 1 addition & 0 deletions AppExamples/CleverDeal.React/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
registry=https://registry.npmjs.org
7 changes: 6 additions & 1 deletion AppExamples/CleverDeal.React/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"private": true,
"dependencies": {
"@symphony-ui/uitoolkit-components": "^3.5.0",
"@tanstack/react-table": "^8.21.3",
"@testing-library/jest-dom": "^5.16.2",
"@testing-library/react": "^12.1.3",
"@testing-library/user-event": "^13.5.0",
Expand Down Expand Up @@ -53,6 +54,10 @@
]
},
"devDependencies": {
"@types/react-tabs": "^2.3.4"
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@types/react-tabs": "^2.3.4",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.49",
"tailwindcss": "^3.4.16"
}
}
6 changes: 6 additions & 0 deletions AppExamples/CleverDeal.React/postcss.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = {
plugins: [
require('tailwindcss'),
require('autoprefixer'),
],
}
26 changes: 26 additions & 0 deletions AppExamples/CleverDeal.React/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,32 @@
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Clever Deal 2.0</title>
<!--
Suppress cross-origin "Script error." events from the Symphony ECP SDK
iframe. Must run before bundle.js so our listener is registered first
and can call stopImmediatePropagation before CRA's overlay handler.
-->
<script>
(function () {
if (!document.getElementById('symphony-ecm')) {
return;
}

Comment on lines +35 to +38
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

The guard if (!document.getElementById('symphony-ecm')) return; runs while the script is executing in <head>, before the <div id="symphony-ecm"> in <body> has been parsed. As a result, the error listener is never registered and cross-origin "Script error." events will not be suppressed as intended. Consider removing the DOM check entirely, or deferring this setup until after DOMContentLoaded (or by placing the script after the symphony-ecm element).

Suggested change
if (!document.getElementById('symphony-ecm')) {
return;
}

Copilot uses AI. Check for mistakes.
window.addEventListener(
'error',
function (e) {
Comment on lines +35 to +41
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

The guard if (!document.getElementById('symphony-ecm')) return; runs in the <head> before the <body> is parsed, so it will always early-return and the error listener won’t be registered. This means cross-origin "Script error." events from the Symphony iframe will still reach CRA’s overlay.

Fix by registering the listener unconditionally, or by moving the element-presence check into the handler (check at error time rather than at script-eval time).

Suggested change
if (!document.getElementById('symphony-ecm')) {
return;
}
window.addEventListener(
'error',
function (e) {
window.addEventListener(
'error',
function (e) {
if (!document.getElementById('symphony-ecm')) {
return;
}

Copilot uses AI. Check for mistakes.
if (
(e.message === 'Script error.' || e.message === 'Script error') &&
e.filename === ''
) {
e.stopImmediatePropagation();
e.preventDefault();
}
},
true,
);
})();
</script>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
Expand Down
318 changes: 318 additions & 0 deletions AppExamples/CleverDeal.React/public/wealth-client-chat-host.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,318 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Wealth Client Chat Host</title>
<style>
html,
body,
#wealth-client-chat {
height: 100%;
width: 100%;
margin: 0;
padding: 0;
overflow: hidden;
background: #fbfcfe;
}

iframe {
border: 0;
}
</style>
</head>
<body>
<div id="wealth-client-chat"></div>
<script>
(function () {
const DEFAULT_ORIGIN = 'corporate.symphony.com';
const DEFAULT_PARTNER_ID = 'symphony_internal_BYC-XXX';
const CONTAINER_ID = 'wealth-client-chat';
const ALLOWED_APPS = 'com.symphony.zoom,com.symphony.teams,salesforce2-app,com.symphony.sfs.admin-app';
const query = new URLSearchParams(window.location.search);
const ecpOrigin = query.get('ecpOrigin') || DEFAULT_ORIGIN;
const partnerId = query.get('partnerId');
const mode = query.get('mode') || 'light';
const theme = query.get('theme');
const podUrl = 'https://' + ecpOrigin;
const initialStreamId = query.get('streamId');
Comment on lines +32 to +38
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

ecpOrigin is taken directly from the query string and used to construct podUrl/iframe URLs (e.g. https://${ecpOrigin}/embed/index.html). This allows embedding arbitrary remote origins and is effectively a client-side SSRF / script-inclusion vector if this page is reachable with attacker-controlled params. Please validate ecpOrigin against a strict allowlist (e.g. the known Symphony pods) before using it, and fall back to DEFAULT_ORIGIN when invalid.

Copilot uses AI. Check for mistakes.
let activeStreamId = initialStreamId;
let frame = null;
let isClientReady = false;
let actionId = 0;
const pendingSendActions = {};

function debug(message, context) {
if (context) {
console.debug('[WealthClientHost] ' + message, context);
return;
}

console.debug('[WealthClientHost] ' + message);
}

function warn(message, context) {
if (context) {
console.warn('[WealthClientHost] ' + message, context);
return;
}

console.warn('[WealthClientHost] ' + message);
}

function error(message, context) {
if (context) {
console.error('[WealthClientHost] ' + message, context);
return;
}

console.error('[WealthClientHost] ' + message);
}

function postToParent(type, payload) {
debug('Posting host event to parent.', { type: type, payload: payload || {} });
window.parent.postMessage(
{
source: 'wealth-client-chat-host',
type: type,
payload: payload || {},
},
window.location.origin,
);
}

function buildFrameUrl(streamId) {
const url = new URL('https://' + ecpOrigin + '/embed/index.html');
url.searchParams.set('streamId', streamId);
url.searchParams.set('module', 'room');
url.searchParams.set('mode', mode);
url.searchParams.set('condensed', 'false');
url.searchParams.set('showTitle', 'false');
url.searchParams.set('canAddPeople', 'true');
url.searchParams.set('ecpLoginPopup', 'true');
url.searchParams.set('allowedApps', ALLOWED_APPS);
url.searchParams.set('sound', 'false');
url.searchParams.set('showInfo', 'false');
url.searchParams.set('showMembers', 'false');
url.searchParams.set('symphonyLogo', 'false');
url.searchParams.set('sdkOrigin', window.location.origin);
url.searchParams.set('embed', 'true');
if (theme) {
url.searchParams.set('theme', theme);
}
if (partnerId) {
url.searchParams.set('partnerId', partnerId);
} else if (ecpOrigin !== 'st3.dev.symphony.com') {
url.searchParams.set('partnerId', DEFAULT_PARTNER_ID);
}
debug('Built client chat frame URL.', {
streamId: streamId,
ecpOrigin: ecpOrigin,
mode: mode,
hasTheme: Boolean(theme),
url: url.toString(),
});
return url.toString();
}

function ensureFrame() {
if (frame) {
return frame;
}

frame = document.createElement('iframe');
frame.setAttribute('title', 'Wealth Client Chat Frame');
frame.setAttribute('style', 'width:100%;height:100%;border:0;');
frame.addEventListener('load', function () {
debug('Client chat iframe load event fired.', {
activeStreamId: activeStreamId,
src: frame ? frame.src : null,
});
});
document.getElementById(CONTAINER_ID).replaceChildren(frame);
debug('Created client chat iframe container.', { containerId: CONTAINER_ID });
return frame;
}

function openStream(streamId) {
if (!streamId) {
debug('Skipped openStream because stream id was empty.');
return;
}

if (activeStreamId === streamId && frame) {
debug('Skipped iframe navigation because stream is already active.', {
streamId: streamId,
});
if (isClientReady) {
postToParent('ready', { streamId: activeStreamId });
}
return;
}

activeStreamId = streamId;
isClientReady = false;
debug('Opening client chat stream in iframe.', {
streamId: streamId,
previousStreamId: frame ? frame.getAttribute('data-stream-id') : null,
});
ensureFrame().setAttribute('data-stream-id', streamId);
ensureFrame().src = buildFrameUrl(streamId);
}

function postEcpMessage(eventType, payload) {
const targetFrame = ensureFrame();
if (!targetFrame.contentWindow) {
throw new Error('Client chat iframe window is not available.');
}

debug('Posting message to embedded ECP frame.', {
eventType: eventType,
payload: payload,
targetOrigin: podUrl,
});
targetFrame.contentWindow.postMessage({ eventType: eventType, payload: payload }, podUrl);
}

function sendMessageToStream(requestId, documentName, messagePayload, streamId) {
const targetStreamId = streamId || activeStreamId;
if (!targetStreamId) {
throw new Error('Client chat stream id is missing.');
}

if (!isClientReady) {
throw new Error('Client chat is not ready yet.');
}

const actionRequestId = 'send-message-' + ++actionId;
pendingSendActions[actionRequestId] = {
requestId: requestId,
documentName: documentName,
streamId: targetStreamId,
};

debug('Sending document share request to embedded client chat.', {
requestId: requestId,
actionRequestId: actionRequestId,
documentName: documentName,
streamId: targetStreamId,
messagePayload: messagePayload,
});

postEcpMessage('sdk-action', {
name: 'send-message',
id: actionRequestId,
params: {
message: messagePayload,
options: {
mode: 'blast',
streamIds: [targetStreamId],
},
},
});
}

window.addEventListener('message', function (event) {
if (event.origin === podUrl) {
const ecpData = event.data || {};
debug('Received embedded ECP event.', ecpData);

if (ecpData.eventType === 'clientReady') {
isClientReady = true;
postToParent('ready', { streamId: activeStreamId });
return;
}

if (ecpData.eventType === 'sdk-resolve') {
const payload = ecpData.payload || {};
const pendingAction = pendingSendActions[payload.id];
if (!pendingAction) {
return;
}

delete pendingSendActions[payload.id];

if (payload.data && payload.data.error) {
error('Embedded ECP send-message failed.', {
actionRequestId: payload.id,
requestId: pendingAction.requestId,
documentName: pendingAction.documentName,
error: payload.data.error,
});
postToParent('share-error', {
requestId: pendingAction.requestId,
documentName: pendingAction.documentName,
message: payload.data.error.message || 'Unable to share document to client chat.',
});
return;
}

debug('Embedded ECP send-message succeeded.', {
actionRequestId: payload.id,
requestId: pendingAction.requestId,
documentName: pendingAction.documentName,
streamId: pendingAction.streamId,
});
postToParent('share-success', {
requestId: pendingAction.requestId,
documentName: pendingAction.documentName,
streamId: pendingAction.streamId,
});
return;
}

return;
}

if (event.origin !== window.location.origin) {
return;
}

const data = event.data || {};
if (data.source !== 'wealth-client-chat-parent') {
return;
}

debug('Received parent message.', {
type: data.type,
payload: data.payload || {},
});

if (data.type === 'open-stream') {
openStream(data.payload && data.payload.streamId);
return;
}

if (data.type === 'send-message') {
try {
sendMessageToStream(
data.payload && data.payload.requestId,
data.payload && data.payload.documentName,
data.payload && data.payload.message,
data.payload && data.payload.streamId,
);
} catch (sendError) {
const message = sendError instanceof Error ? sendError.message : 'Unable to share document to client chat.';
warn('Unable to forward share request to embedded client chat.', {
error: message,
payload: data.payload || {},
});
postToParent('share-error', {
requestId: data.payload && data.payload.requestId,
documentName: data.payload && data.payload.documentName,
message: message,
});
}
}
});

debug('Initializing wealth client chat host.', {
ecpOrigin: ecpOrigin,
initialStreamId: initialStreamId,
partnerId: partnerId || DEFAULT_PARTNER_ID,
});
openStream(initialStreamId);
})();
</script>
</body>
</html>
Loading
Loading