diff --git a/packages/node_modules/@webex/private-react-component-token-input/src/index.js b/packages/node_modules/@webex/private-react-component-token-input/src/index.js
index 472c4adb4..52d1dd531 100644
--- a/packages/node_modules/@webex/private-react-component-token-input/src/index.js
+++ b/packages/node_modules/@webex/private-react-component-token-input/src/index.js
@@ -126,7 +126,7 @@ function TokenInput(props) {
placeholder={tokenType === 'JWT' ? 'JWT' : 'Access Token'}
value={userAccessToken}
/>
-
You can get an access token from developer.webex.com
+ You can get an access token from developer.webex.com
}
diff --git a/packages/node_modules/@webex/private-react-component-token-input/src/index.test.js b/packages/node_modules/@webex/private-react-component-token-input/src/index.test.js
new file mode 100644
index 000000000..44dbfc8c1
--- /dev/null
+++ b/packages/node_modules/@webex/private-react-component-token-input/src/index.test.js
@@ -0,0 +1,17 @@
+import React from 'react';
+import TestRenderer from 'react-test-renderer';
+
+import TokenInput from './index';
+
+describe('developer portal link', () => {
+ it('developer link uses https and safe attributes', () => {
+ const testRenderer = TestRenderer.create();
+ const anchors = testRenderer.root.findAllByType('a');
+ const developerLink = anchors.find((anchor) => anchor.props.href.includes('developer.webex.com'));
+
+ expect(developerLink.props.href).toBe('https://developer.webex.com');
+ expect(developerLink.props.href).not.toMatch(/^http:\/\//);
+ expect(developerLink.props.rel).toContain('noopener');
+ expect(developerLink.props.rel).toContain('noreferrer');
+ });
+});
diff --git a/packages/node_modules/@webex/widget-demo/src/components/demo-widget/index.js b/packages/node_modules/@webex/widget-demo/src/components/demo-widget/index.js
index 35a7eabd8..38e6ee7d9 100644
--- a/packages/node_modules/@webex/widget-demo/src/components/demo-widget/index.js
+++ b/packages/node_modules/@webex/widget-demo/src/components/demo-widget/index.js
@@ -29,6 +29,38 @@ const {
const spaceWidgetElementId = 'my-webex-space-widget';
const recentsWidgetElementId = 'my-webex-recents-widget';
+// Comfortably captures a debugging window without unbounded memory growth.
+const MAX_CAPTURED_EVENTS = 50;
+const REDACTED_EVENT_DETAIL_KEYS = ['id', 'personId', 'personEmail', 'displayName', 'email', 'text', 'message', 'content'];
+
+function redactEventDetail(detail) {
+ if (!detail || typeof detail !== 'object') {
+ return detail;
+ }
+
+ const redacted = {...detail};
+
+ REDACTED_EVENT_DETAIL_KEYS.forEach((key) => {
+ delete redacted[key];
+ });
+
+ return redacted;
+}
+
+// Development-only capture; no-op in production so no global buffer exists there.
+function captureDemoEvent(eventName, detail) {
+ if (process.env.NODE_ENV === 'production') {
+ return;
+ }
+
+ window.ciscoSparkEvents = window.ciscoSparkEvents || [];
+ window.ciscoSparkEvents.push({eventName, detail: redactEventDetail(detail)});
+
+ if (window.ciscoSparkEvents.length > MAX_CAPTURED_EVENTS) {
+ window.ciscoSparkEvents.splice(0, window.ciscoSparkEvents.length - MAX_CAPTURED_EVENTS);
+ }
+}
+
class DemoWidget extends Component {
constructor(props) {
super(props);
@@ -53,8 +85,8 @@ class DemoWidget extends Component {
message: isMeetOnly ? false : activities.message,
people: activities.people
},
- accessToken: cookies.get('accessToken') || '',
- accessTokenType: cookies.get('accessTokenType') || '',
+ accessToken: '',
+ accessTokenType: '',
composerActions,
destinationId,
disableFlags: false,
@@ -98,8 +130,6 @@ class DemoWidget extends Component {
e.preventDefault();
const {cookies} = this.props;
- cookies.set('accessToken', this.state.accessToken);
- cookies.set('accessTokenType', this.state.accessTokenType);
cookies.set('activities', this.state.activities);
cookies.set('destinationId', this.state.destinationId);
cookies.set('destinationMode', this.state.mode);
@@ -124,8 +154,6 @@ class DemoWidget extends Component {
e.preventDefault();
const {cookies} = this.props;
- cookies.set('accessToken', this.state.accessToken);
- cookies.set('accessTokenType', this.state.accessTokenType);
cookies.set('fedramp', this.state.fedramp);
cookies.set('enableSpaceListFilter', this.state.enableSpaceListFilter);
cookies.set('recentsBasicMode', this.state.recentsBasicMode);
@@ -143,7 +171,7 @@ class DemoWidget extends Component {
fedramp: this.state.fedramp,
spaceLoadCount: Number(this.state.spaceLoadCount),
onEvent: (eventName, detail) => {
- window.ciscoSparkEvents.push({eventName, detail});
+ captureDemoEvent(eventName, detail);
if (eventName === 'rooms:selected') {
const spaceId = detail.id;
@@ -353,7 +381,7 @@ class DemoWidget extends Component {
secondaryActivitiesFullWidth: this.state.secondaryActivitiesFullWidth,
spaceActivities: this.state.activities,
onEvent: (eventName, detail) => {
- window.ciscoSparkEvents.push({eventName, detail});
+ captureDemoEvent(eventName, detail);
if (eventName === spaceEvents.ACTIVITY_CHANGED) {
this.setState({setCurrentActivity: ''});
}
diff --git a/packages/node_modules/@webex/widget-demo/src/components/demo-widget/index.test.js b/packages/node_modules/@webex/widget-demo/src/components/demo-widget/index.test.js
new file mode 100644
index 000000000..e2e7b4a18
--- /dev/null
+++ b/packages/node_modules/@webex/widget-demo/src/components/demo-widget/index.test.js
@@ -0,0 +1,130 @@
+import React from 'react';
+import TestRenderer from 'react-test-renderer';
+
+import WrappedDemoWidget from './index';
+
+jest.mock('@webex/widget-space', () => () => null);
+jest.mock('@webex/widget-recents', () => () => null);
+jest.mock('@webex/private-react-component-token-input', () => () => null, {virtual: true});
+jest.mock('@webex/private-react-component-example-code', () => () => null, {virtual: true});
+jest.mock('@webex/private-react-component-space-destination', () => ({
+ __esModule: true,
+ default: () => null,
+ constants: {
+ DESTINATION_PROP_MODE_LEGACY: 'DESTINATION_PROP_MODE_LEGACY',
+ DESTINATION_PROP_MODE_MAIN: 'DESTINATION_PROP_MODE_MAIN',
+ MODE_ONE_ON_ONE: 'email',
+ MODE_ONE_ON_ONE_ID: 'userId',
+ MODE_SPACE: 'spaceId',
+ MODE_SIP: 'sip',
+ MODE_PSTN: 'pstn'
+ }
+}), {virtual: true});
+
+const DemoWidget = WrappedDemoWidget.WrapperComponent;
+
+function createCookiesStub() {
+ const store = {};
+
+ return {
+ get: jest.fn((key) => store[key]),
+ set: jest.fn((key, value) => {
+ store[key] = value;
+ }),
+ store
+ };
+}
+
+function preventDefaultEvent() {
+ return {preventDefault: jest.fn()};
+}
+
+describe('ciscoSparkEvents capture', () => {
+ const originalNodeEnv = process.env.NODE_ENV;
+
+ afterEach(() => {
+ process.env.NODE_ENV = originalNodeEnv;
+ delete window.ciscoSparkEvents;
+ });
+
+ it('no global buffer capture in production', async () => {
+ process.env.NODE_ENV = 'production';
+
+ const cookies = createCookiesStub();
+ const testRenderer = TestRenderer.create();
+ const instance = testRenderer.getInstance();
+
+ await instance.handleOpenRecentsWidget(preventDefaultEvent());
+ instance.state.recentsWidgetProps.onEvent('rooms:selected', {id: 'space-1'});
+
+ expect(window.ciscoSparkEvents).toBeUndefined();
+ });
+
+ it('development capture is bounded and redacted', async () => {
+ process.env.NODE_ENV = 'test';
+
+ const cookies = createCookiesStub();
+ const testRenderer = TestRenderer.create();
+ const instance = testRenderer.getInstance();
+
+ await instance.handleOpenRecentsWidget(preventDefaultEvent());
+ const {onEvent} = instance.state.recentsWidgetProps;
+
+ for (let i = 0; i < 60; i += 1) {
+ onEvent('messages:created', {
+ id: `message-${i}`,
+ personEmail: 'guest@example.com',
+ text: 'sensitive message body'
+ });
+ }
+
+ expect(window.ciscoSparkEvents.length).toBeLessThanOrEqual(50);
+ window.ciscoSparkEvents.forEach((entry) => {
+ expect(entry.detail).not.toHaveProperty('id');
+ expect(entry.detail).not.toHaveProperty('personEmail');
+ expect(entry.detail).not.toHaveProperty('text');
+ });
+ });
+});
+
+describe('token cookie storage', () => {
+ it('no raw token cookie or hardened options', async () => {
+ const cookies = createCookiesStub();
+
+ cookies.store.accessToken = 'raw-access-token-value';
+ cookies.store.accessTokenType = 'Bearer';
+
+ const testRenderer = TestRenderer.create();
+ const instance = testRenderer.getInstance();
+
+ expect(instance.state.accessToken).toBe('');
+ expect(instance.state.accessTokenType).toBe('');
+
+ instance.handleOpenSpaceWidget(preventDefaultEvent());
+ await instance.handleOpenRecentsWidget(preventDefaultEvent());
+
+ expect(cookies.set).not.toHaveBeenCalledWith('accessToken', expect.anything());
+ expect(cookies.set).not.toHaveBeenCalledWith('accessTokenType', expect.anything());
+ });
+
+ it('non-token preference cookies preserved', async () => {
+ const cookies = createCookiesStub();
+ const testRenderer = TestRenderer.create();
+ const instance = testRenderer.getInstance();
+
+ instance.handleOpenSpaceWidget(preventDefaultEvent());
+ await instance.handleOpenRecentsWidget(preventDefaultEvent());
+
+ expect(cookies.set).toHaveBeenCalledWith('destinationMode', expect.anything());
+ expect(cookies.set).toHaveBeenCalledWith('activities', expect.anything());
+ expect(cookies.set).toHaveBeenCalledWith('destinationId', expect.anything());
+ expect(cookies.set).toHaveBeenCalledWith('fedramp', expect.anything());
+ expect(cookies.set).toHaveBeenCalledWith('initialActivity', expect.anything());
+ expect(cookies.set).toHaveBeenCalledWith('enableSpaceListFilter', expect.anything());
+ expect(cookies.set).toHaveBeenCalledWith('recentsBasicMode', expect.anything());
+ expect(cookies.set).toHaveBeenCalledWith('recentsEnableAddButton', expect.anything());
+ expect(cookies.set).toHaveBeenCalledWith('recentsEnableUserProfile', expect.anything());
+ expect(cookies.set).toHaveBeenCalledWith('recentsEnableUserProfileMenu', expect.anything());
+ expect(cookies.set).toHaveBeenCalledWith('spaceLoadCount', expect.anything());
+ });
+});
diff --git a/packages/node_modules/@webex/widget-demo/src/components/demo-widget/sdk.js b/packages/node_modules/@webex/widget-demo/src/components/demo-widget/sdk.js
index 1ffa81dc1..bd6b3ef14 100644
--- a/packages/node_modules/@webex/widget-demo/src/components/demo-widget/sdk.js
+++ b/packages/node_modules/@webex/widget-demo/src/components/demo-widget/sdk.js
@@ -113,6 +113,10 @@ export function createSDKInstance(accessToken, options = {}) {
return Promise.resolve(webexSDKInstance);
}
+// Comfortably exceeds real guest-issuer JWT lengths; bounds client-side input
+// before it reaches the Identity Broker, without parsing or trusting claims.
+const MAX_GUEST_JWT_LENGTH = 8192;
+
/**
* Creates a webex instance with the jwt token generated
* by a guest issuer.
@@ -123,6 +127,12 @@ export function createSDKInstance(accessToken, options = {}) {
* @returns {Promise