From e22915708829600ad850fb24cbad21038dd8a0cf Mon Sep 17 00:00:00 2001 From: Ravi Chandra Sekhar Sarika Date: Tue, 14 Apr 2026 21:52:12 +0530 Subject: [PATCH 01/10] fix(internal-plugin-mobius-socket): update websocket request flow --- .../src/config.js | 7 +- .../src/mobius-socket.js | 91 +++++++- .../src/socket/socket-base.js | 220 ++++++++++++++---- .../test/unit/spec/mobius-socket-events.js | 23 +- .../test/unit/spec/mobius-socket.js | 146 +++++++++++- .../test/unit/spec/socket.js | 84 ++++--- 6 files changed, 463 insertions(+), 108 deletions(-) diff --git a/packages/@webex/internal-plugin-mobius-socket/src/config.js b/packages/@webex/internal-plugin-mobius-socket/src/config.js index b9dc915770f..0f79e3b84f7 100644 --- a/packages/@webex/internal-plugin-mobius-socket/src/config.js +++ b/packages/@webex/internal-plugin-mobius-socket/src/config.js @@ -4,10 +4,15 @@ const mobiusSocketConfig = { /** - * Milliseconds to wait for auth.response before declaring auth failed + * Milliseconds to wait for the auth websocket response before declaring auth failed * @type {number} */ authResponseTimeout: process.env.MOBIUS_SOCKET_AUTH_RESPONSE_TIMEOUT || 10000, + /** + * Milliseconds to wait for a request/response style websocket message. + * @type {number} + */ + wssResponseTimeout: process.env.MOBIUS_SOCKET_RESPONSE_TIMEOUT || 10000, /** * Maximum milliseconds between connection attempts * @type {Number} diff --git a/packages/@webex/internal-plugin-mobius-socket/src/mobius-socket.js b/packages/@webex/internal-plugin-mobius-socket/src/mobius-socket.js index af1de5baaa2..f1ba5219240 100644 --- a/packages/@webex/internal-plugin-mobius-socket/src/mobius-socket.js +++ b/packages/@webex/internal-plugin-mobius-socket/src/mobius-socket.js @@ -20,6 +20,14 @@ import { const normalReconnectReasons = ['idle', 'done (forced)']; const DEFAULT_MOBIUS_WEBSOCKET_SESSION = 'mobius-websocket-session'; +function normalizeMobiusAuthToken(token) { + if (typeof token !== 'string') { + return token; + } + + return token.replace(/^Bearer\s+/i, ''); +} + const MobiusSocket = WebexPlugin.extend({ namespace: 'MobiusSocket', lastError: undefined, @@ -239,6 +247,73 @@ const MobiusSocket = WebexPlugin.extend({ return socket.send(payload); }, + /** + * Sends a websocket request and resolves when the matching response arrives. + * @param {Object} payload - The websocket request payload. + * @param {string|Object} [sessionIdOrOptions=this.defaultSessionId] - Session ID or request options. + * @param {Object} [options={}] - Additional request options. + * @returns {Promise} + */ + sendWssRequest(payload, sessionIdOrOptions = this.defaultSessionId, options = {}) { + if (!payload || typeof payload !== 'object') { + return Promise.reject(new Error('`payload` is required')); + } + + let sessionId = this.defaultSessionId; + let requestOptions = options; + + if (typeof sessionIdOrOptions === 'string') { + sessionId = sessionIdOrOptions; + } else if (sessionIdOrOptions && typeof sessionIdOrOptions === 'object') { + requestOptions = sessionIdOrOptions; + } + + const socket = this.getSocket(sessionId); + + if (!socket || !socket.connected) { + return Promise.reject(new Error(`Mobius socket is not connected for session ${sessionId}`)); + } + + const normalizedPayload = + payload.type === 'auth' && typeof payload?.payload?.token === 'string' + ? { + ...payload, + payload: { + ...payload.payload, + token: normalizeMobiusAuthToken(payload.payload.token), + }, + } + : payload; + + return socket.sendRequest(normalizedPayload, { + timeout: + requestOptions.timeout || + this.config.wssResponseTimeout || + this.config.authResponseTimeout || + 10000, + matchesResponse: (response, request) => + response?.type === 'response_event' && + response?.subtype === request.type && + response?.trackingId === request.trackingId, + getStatusCode: (response) => + response?.statusCode || response?.status?.code || response?.data?.statusCode, + getStatusMessage: (response) => + response?.statusMessage || response?.status?.message || response?.data?.statusMessage, + createError: (response, statusCode, statusMessage) => + this._createWssResponseError(response, statusCode, statusMessage), + createTimeoutError: (request) => + this._createWssResponseError( + { + type: 'response_event', + subtype: request.type, + trackingId: request.trackingId, + }, + 408, + 'Mobius websocket response timed out' + ), + }); + }, + /** * Check if the plugin is connected * @returns {boolean} True if connected @@ -421,6 +496,20 @@ const MobiusSocket = WebexPlugin.extend({ this.localClusterServiceUrls = message.localClusterServiceUrls; }, + _createWssResponseError(response, statusCode, statusMessage) { + const error = new Error( + statusMessage || `Mobius websocket request failed with status ${statusCode || 'unknown'}` + ); + + error.name = 'MobiusSocketResponseError'; + error.statusCode = statusCode; + error.statusMessage = statusMessage; + error.response = response; + error.trackingId = response?.trackingId; + + return error; + }, + _applyOverrides(event) { if (!event || !event.headers) { return; @@ -589,7 +678,7 @@ const MobiusSocket = WebexPlugin.extend({ let options = { forceCloseDelay: this.config.forceCloseDelay, authResponseTimeout: this.config.authResponseTimeout, - token: token.toString(), + token: normalizeMobiusAuthToken(token.toString()), trackingId: `${this.webex.sessionId}_${Date.now()}`, logger: this.logger, }; diff --git a/packages/@webex/internal-plugin-mobius-socket/src/socket/socket-base.js b/packages/@webex/internal-plugin-mobius-socket/src/socket/socket-base.js index 729056900ef..f61a3c3940b 100644 --- a/packages/@webex/internal-plugin-mobius-socket/src/socket/socket-base.js +++ b/packages/@webex/internal-plugin-mobius-socket/src/socket/socket-base.js @@ -32,6 +32,7 @@ export default class Socket extends EventEmitter { constructor() { super(); this._domain = 'unknown-domain'; + this._pendingResponses = new Map(); this.onmessage = this.onmessage.bind(this); this.onclose = this.onclose.bind(this); // Increase max listeners to avoid memory leak warning in tests @@ -293,6 +294,7 @@ export default class Socket extends EventEmitter { this.logger.info(`socket,${this._domain}: closed`, event.code, event.reason); event = this._fixCloseCode(event); + this._rejectPendingResponses(new ConnectionError(event)); this.emit('close', event); // Remove all listeners to (a) avoid reacting to late pongs and (b) ensure @@ -316,6 +318,7 @@ export default class Socket extends EventEmitter { }); } + this._handlePendingResponse(data); this.emit('message', processedEvent); } catch (error) { /* istanbul ignore next */ @@ -346,6 +349,88 @@ export default class Socket extends EventEmitter { }); } + /** + * Sends a request and resolves when the matching response arrives. + * @param {Object} data + * @param {Object} [options={}] + * @param {Function} [options.matchesResponse] + * @param {Function} [options.createError] + * @param {Function} [options.createTimeoutError] + * @param {Function} [options.getStatusCode] + * @param {Function} [options.getStatusMessage] + * @param {number} [options.timeout] + * @returns {Promise} + */ + sendRequest(data, options = {}) { + if (!isObject(data)) { + return Promise.reject(new Error('`data` is required')); + } + + const request = {...data}; + const trackingId = request.trackingId || this._createTrackingId(); + const timeout = options.timeout || this.authResponseTimeout || 10000; + const matchesResponse = + options.matchesResponse || + ((response) => response?.trackingId === trackingId && response?.type === 'response_event'); + const getStatusCode = + options.getStatusCode || + ((response) => response?.statusCode || response?.status?.code || response?.data?.statusCode); + const getStatusMessage = + options.getStatusMessage || + ((response) => + response?.statusMessage || response?.status?.message || response?.data?.statusMessage); + const createError = + options.createError || + ((response, statusCode, statusMessage) => + new ConnectionError({ + code: statusCode, + reason: statusMessage || response?.reason || 'Socket request failed', + })); + const createTimeoutError = + options.createTimeoutError || + (() => + new ConnectionError({ + reason: 'Socket response not received before timeout', + })); + + if (this._pendingResponses.has(trackingId)) { + return Promise.reject( + new Error(`socket request already pending for trackingId ${trackingId}`) + ); + } + + request.trackingId = trackingId; + + return new Promise((resolve, reject) => { + const timeoutId = safeSetTimeout(() => { + this._clearPendingResponse(trackingId); + reject(createTimeoutError(request)); + }, timeout); + + this._pendingResponses.set(trackingId, { + request, + matchesResponse, + getStatusCode, + getStatusMessage, + createError, + resolve: (response) => { + this._clearPendingResponse(trackingId); + resolve(response); + }, + reject: (error) => { + this._clearPendingResponse(trackingId); + reject(error); + }, + timeoutId, + }); + + this.send(request).catch((error) => { + this._clearPendingResponse(trackingId); + reject(error); + }); + }); + } + /** * Sends an acknowledgment for a specific event * @param {MessageEvent} event @@ -383,59 +468,34 @@ export default class Socket extends EventEmitter { * @returns {Promise} */ _authorize() { - return new Promise((resolve, reject) => { - this.logger.info(`socket,${this._domain}: authorizing`); - let authResponseTimer; + this.logger.info(`socket,${this._domain}: authorizing`); - const cleanup = () => { - clearTimeout(authResponseTimer); - this.off('message', waitForAuthResponse); - }; - - const waitForAuthResponse = (event) => { - if (event.data?.type !== MESSAGE_TYPES.AUTH_RESPONSE) { - return; - } - - cleanup(); - - const statusCode = event.data?.status?.code; - - if (statusCode >= 200 && statusCode < 300) { - resolve(); - - return; - } - - reject( + return this.sendRequest( + { + type: MESSAGE_TYPES.AUTH, + data: { + token: this.token, + }, + }, + { + timeout: this.authResponseTimeout || 10000, + matchesResponse: (response, request) => + response?.type === 'response_event' && + response?.subtype === MESSAGE_TYPES.AUTH && + response?.trackingId === request.trackingId, + getStatusCode: (response) => response?.statusCode, + getStatusMessage: (response) => response?.statusMessage, + createError: (response, statusCode, statusMessage) => new NotAuthorized({ code: statusCode, - reason: event.data?.status?.message || 'Mobius auth failed', - }) - ); - }; - - this.on('message', waitForAuthResponse); - authResponseTimer = safeSetTimeout(() => { - cleanup(); - reject( + reason: statusMessage || 'Mobius auth failed', + }), + createTimeoutError: () => new NotAuthorized({ reason: 'Mobius auth response not received before timeout', - }) - ); - }, this.authResponseTimeout || 10000); - - this.send({ - type: MESSAGE_TYPES.AUTH, - trackingId: this._createTrackingId(), - payload: { - token: this.token, - }, - }).catch((error) => { - cleanup(); - reject(error); - }); - }); + }), + } + ); } /** @@ -447,6 +507,70 @@ export default class Socket extends EventEmitter { return `${this.trackingId}_${uuid.v4()}`; } + /** + * Clears a pending response entry. + * @param {string} trackingId + * @returns {void} + */ + _clearPendingResponse(trackingId) { + const pendingResponse = this._pendingResponses.get(trackingId); + + if (pendingResponse?.timeoutId) { + clearTimeout(pendingResponse.timeoutId); + } + + this._pendingResponses.delete(trackingId); + } + + /** + * Rejects all pending responses with the provided error. + * @param {Error} error + * @returns {void} + */ + _rejectPendingResponses(error) { + if (!this._pendingResponses.size) { + return; + } + + Array.from(this._pendingResponses.values()).forEach((pendingResponse) => { + pendingResponse.reject(error); + }); + } + + /** + * Handles incoming responses for pending requests. + * @param {Object} response + * @returns {boolean} + */ + _handlePendingResponse(response) { + if (!response) { + return false; + } + + const pendingResponse = response.trackingId + ? this._pendingResponses.get(response.trackingId) + : undefined; + + if (!pendingResponse) { + return false; + } + + if (!pendingResponse.matchesResponse(response, pendingResponse.request)) { + return false; + } + + const statusCode = pendingResponse.getStatusCode(response); + const statusMessage = pendingResponse.getStatusMessage(response); + + if (statusCode >= 200 && statusCode < 300) { + pendingResponse.resolve(response); + } else { + pendingResponse.reject(pendingResponse.createError(response, statusCode, statusMessage)); + } + + return true; + } + /** * Deals with the fact that some browsers drop some close codes (but not * close reasons). diff --git a/packages/@webex/internal-plugin-mobius-socket/test/unit/spec/mobius-socket-events.js b/packages/@webex/internal-plugin-mobius-socket/test/unit/spec/mobius-socket-events.js index 68ab85c301e..c0a74c4b6e9 100644 --- a/packages/@webex/internal-plugin-mobius-socket/test/unit/spec/mobius-socket-events.js +++ b/packages/@webex/internal-plugin-mobius-socket/test/unit/spec/mobius-socket-events.js @@ -42,6 +42,20 @@ describe('plugin-mobiusSocket', () => { sessionId: 'mobius-websocket-session', }; + const emitAuthResponse = ({statusCode = 200, statusMessage = 'OK'} = {}) => { + const authRequest = JSON.parse(mockWebSocket.send.lastCall.args[0]); + + mockWebSocket.emit('message', { + data: JSON.stringify({ + type: 'response_event', + subtype: MESSAGE_TYPES.AUTH, + trackingId: authRequest.trackingId, + statusCode, + statusMessage, + }), + }); + }; + beforeEach(() => { clock = FakeTimers.install({now: Date.now()}); }); @@ -90,14 +104,9 @@ describe('plugin-mobiusSocket', () => { process.nextTick(() => { mockWebSocket.open(); - // Simulate Mobius auth.response after socket open + // Simulate Mobius auth response after socket open process.nextTick(() => { - mockWebSocket.emit('message', { - data: JSON.stringify({ - type: MESSAGE_TYPES.AUTH_RESPONSE, - status: {code: 200}, - }), - }); + emitAuthResponse(); }); }); diff --git a/packages/@webex/internal-plugin-mobius-socket/test/unit/spec/mobius-socket.js b/packages/@webex/internal-plugin-mobius-socket/test/unit/spec/mobius-socket.js index 60cfc4561fb..f9b13718e6e 100644 --- a/packages/@webex/internal-plugin-mobius-socket/test/unit/spec/mobius-socket.js +++ b/packages/@webex/internal-plugin-mobius-socket/test/unit/spec/mobius-socket.js @@ -40,6 +40,20 @@ describe('plugin-mobius-socket', () => { trackingId: `suffix_${uuid.v4()}_${Date.now()}`, }); + const emitAuthResponse = ({statusCode = 200, statusMessage = 'OK'} = {}) => { + const authRequest = JSON.parse(mockWebSocket.send.lastCall.args[0]); + + mockWebSocket.emit('message', { + data: JSON.stringify({ + type: 'response_event', + subtype: MESSAGE_TYPES.AUTH, + trackingId: authRequest.trackingId, + statusCode, + statusMessage, + }), + }); + }; + beforeEach(() => { clock = FakeTimers.install({now: Date.now()}); }); @@ -97,14 +111,9 @@ describe('plugin-mobius-socket', () => { process.nextTick(() => { mockWebSocket.open(); - // Simulate Mobius auth.response after socket open + // Simulate Mobius auth response after socket open process.nextTick(() => { - mockWebSocket.emit('message', { - data: JSON.stringify({ - type: MESSAGE_TYPES.AUTH_RESPONSE, - status: {code: 200}, - }), - }); + emitAuthResponse(); }); }); @@ -806,6 +815,129 @@ describe('plugin-mobius-socket', () => { }); }); + describe('#sendWssRequest()', () => { + beforeEach(() => { + mobiusSocket.config.wssResponseTimeout = 100; + }); + + it('resolves when a matching response_event arrives', async () => { + await mobiusSocket.connect(); + + const requestPromise = mobiusSocket.sendWssRequest({ + type: 'auth', + payload: { + token: 'Bearer test', + }, + }); + + await promiseTick(); + + const requestPayload = JSON.parse(mockWebSocket.send.lastCall.args[0]); + + assert.equal(requestPayload.payload.token, 'test'); + + mockWebSocket.emit('message', { + data: JSON.stringify({ + type: 'response_event', + subtype: 'auth', + trackingId: requestPayload.trackingId, + statusCode: 200, + statusMessage: 'OK', + }), + }); + + const response = await requestPromise; + + assert.equal(response.type, 'response_event'); + assert.equal(response.subtype, 'auth'); + assert.equal(response.trackingId, requestPayload.trackingId); + assert.equal(response.statusCode, 200); + }); + + it('strips the Bearer prefix from connect-time auth token', async () => { + await mobiusSocket.connect(); + + const authPayload = JSON.parse(mockWebSocket.send.firstCall.args[0]); + + assert.equal(authPayload.type, MESSAGE_TYPES.AUTH); + assert.equal(authPayload.payload.token, 'FAKE'); + }); + + it('rejects when a matching response_event is non-2xx', async () => { + await mobiusSocket.connect(); + + const requestPromise = mobiusSocket.sendWssRequest({ + type: 'auth', + payload: { + token: 'Bearer test', + }, + }); + + await promiseTick(); + + const requestPayload = JSON.parse(mockWebSocket.send.lastCall.args[0]); + + mockWebSocket.emit('message', { + data: JSON.stringify({ + type: 'response_event', + subtype: 'auth', + trackingId: requestPayload.trackingId, + statusCode: 403, + statusMessage: 'Forbidden', + }), + }); + + await assert.isRejected(requestPromise).then((error) => { + assert.equal(error.name, 'MobiusSocketResponseError'); + assert.equal(error.statusCode, 403); + assert.equal(error.statusMessage, 'Forbidden'); + assert.equal(error.trackingId, requestPayload.trackingId); + }); + }); + + it('rejects when the matching response does not arrive before timeout', async () => { + await mobiusSocket.connect(); + + const requestPromise = mobiusSocket.sendWssRequest({ + type: 'auth', + payload: { + token: 'Bearer test', + }, + }); + + clock.tick(101); + await promiseTick(); + + await assert.isRejected(requestPromise).then((error) => { + assert.equal(error.name, 'MobiusSocketResponseError'); + assert.equal(error.statusCode, 408); + assert.equal(error.statusMessage, 'Mobius websocket response timed out'); + }); + }); + + it('rejects pending requests when the active socket closes', async () => { + await mobiusSocket.connect(); + + const requestPromise = mobiusSocket.sendWssRequest({ + type: 'auth', + payload: { + token: 'Bearer test', + }, + }); + + mockWebSocket.emit('close', { + code: 1003, + reason: 'service rejected request', + }); + + await assert.isRejected(requestPromise).then((error) => { + assert.instanceOf(error, ConnectionError); + assert.equal(error.code, 1003); + assert.equal(error.reason, 'service rejected request'); + }); + }); + }); + describe('#_emit()', () => { it('emits Error-safe events and log the error with the call parameters', () => { const error = 'error'; diff --git a/packages/@webex/internal-plugin-mobius-socket/test/unit/spec/socket.js b/packages/@webex/internal-plugin-mobius-socket/test/unit/spec/socket.js index a8395e44573..506dc13f7cc 100644 --- a/packages/@webex/internal-plugin-mobius-socket/test/unit/spec/socket.js +++ b/packages/@webex/internal-plugin-mobius-socket/test/unit/spec/socket.js @@ -32,6 +32,20 @@ describe('plugin-mobius-socket', () => { config.mobiusSocket ); + const emitAuthResponse = ({statusCode = 200, statusMessage = 'OK'} = {}) => { + const authRequest = JSON.parse(mockWebSocket.send.lastCall.args[0]); + + mockWebSocket.emit('message', { + data: JSON.stringify({ + type: 'response_event', + subtype: MESSAGE_TYPES.AUTH, + trackingId: authRequest.trackingId, + statusCode, + statusMessage, + }), + }); + }; + beforeEach(() => { clock = FakeTimers.install({now: Date.now()}); }); @@ -55,14 +69,9 @@ describe('plugin-mobius-socket', () => { const promise = socket.open('ws://example.com', mockoptions); mockWebSocket.open(); - // Simulate Mobius auth.response (MockWebSocket.open auto-sends mercury.buffer_state which Mobius ignores) + // Simulate Mobius auth response (MockWebSocket.open auto-sends mercury.buffer_state which Mobius ignores) process.nextTick(() => { - mockWebSocket.emit('message', { - data: JSON.stringify({ - type: MESSAGE_TYPES.AUTH_RESPONSE, - status: {code: 200}, - }), - }); + emitAuthResponse(); }); return promise; @@ -135,12 +144,7 @@ describe('plugin-mobius-socket', () => { mockWebSocket.readyState = 1; mockWebSocket.emit('open'); - mockWebSocket.emit('message', { - data: JSON.stringify({ - type: MESSAGE_TYPES.AUTH_RESPONSE, - status: {code: 200}, - }), - }); + emitAuthResponse(); return promise.then(() => { assert.equal(socket.logLevelToken, 'mocklogleveltoken'); @@ -379,21 +383,33 @@ describe('plugin-mobius-socket', () => { }); }); - it('resolves upon receiving auth.response', () => { + it('resolves upon receiving response_event auth response', () => { const s = new Socket(); const promise = s.open('ws://example.com', mockoptions); mockWebSocket.readyState = 1; mockWebSocket.emit('open'); - mockWebSocket.emit('message', { - data: JSON.stringify({ - type: MESSAGE_TYPES.AUTH_RESPONSE, - status: {code: 200}, - }), - }); + emitAuthResponse(); return promise.then(() => s.close()); }); + + it('rejects upon receiving a non-2xx auth response_event', () => { + const s = new Socket(); + const promise = s.open('ws://example.com', mockoptions); + + mockWebSocket.readyState = 1; + mockWebSocket.emit('open'); + emitAuthResponse({statusCode: 401, statusMessage: 'Unauthorized'}); + + return assert.isRejected(promise).then((reason) => { + assert.instanceOf(reason, NotAuthorized); + assert.equal(reason.code, 401); + assert.match(reason.reason, /Unauthorized/); + + return s.close(); + }); + }); }); }); @@ -440,12 +456,7 @@ describe('plugin-mobius-socket', () => { mockWebSocket.readyState = 1; mockWebSocket.emit('open'); - mockWebSocket.emit('message', { - data: JSON.stringify({ - type: MESSAGE_TYPES.AUTH_RESPONSE, - status: {code: 200}, - }), - }); + emitAuthResponse(); return promise.then(() => { const spy = sinon.spy(); @@ -477,12 +488,7 @@ describe('plugin-mobius-socket', () => { mockWebSocket.readyState = 1; mockWebSocket.emit('open'); - mockWebSocket.emit('message', { - data: JSON.stringify({ - type: MESSAGE_TYPES.AUTH_RESPONSE, - status: {code: 200}, - }), - }); + emitAuthResponse(); return promise.then(() => { const spy = sinon.spy(); @@ -514,12 +520,7 @@ describe('plugin-mobius-socket', () => { mockWebSocket.readyState = 1; mockWebSocket.emit('open'); - mockWebSocket.emit('message', { - data: JSON.stringify({ - type: MESSAGE_TYPES.AUTH_RESPONSE, - status: {code: 200}, - }), - }); + emitAuthResponse(); return promise.then(() => { const spy = sinon.spy(); @@ -551,12 +552,7 @@ describe('plugin-mobius-socket', () => { mockWebSocket.readyState = 1; mockWebSocket.emit('open'); - mockWebSocket.emit('message', { - data: JSON.stringify({ - type: MESSAGE_TYPES.AUTH_RESPONSE, - status: {code: 200}, - }), - }); + emitAuthResponse(); return promise.then(() => { const spy = sinon.spy(); From 225b130279e5b9e9d2b11763b8ddb3a5176fde39 Mon Sep 17 00:00:00 2001 From: Ravi Chandra Sekhar Sarika Date: Tue, 14 Apr 2026 22:40:19 +0530 Subject: [PATCH 02/10] fix(internal-plugin-mobius-socket): align websocket request envelope --- .../internal-plugin-mobius-socket/README.md | 8 ++-- .../src/config.js | 12 ++--- .../src/mobius-socket.js | 45 ++++++++++------- .../src/socket/socket-base.js | 4 +- .../test/unit/spec/mobius-socket.js | 48 +++++++++++++++---- .../test/unit/spec/socket.js | 14 +++--- 6 files changed, 86 insertions(+), 45 deletions(-) diff --git a/packages/@webex/internal-plugin-mobius-socket/README.md b/packages/@webex/internal-plugin-mobius-socket/README.md index 4f07f7c77c2..537188cca2d 100644 --- a/packages/@webex/internal-plugin-mobius-socket/README.md +++ b/packages/@webex/internal-plugin-mobius-socket/README.md @@ -62,9 +62,9 @@ await mobiusSocket.disconnect(); Mobius uses a token-based auth handshake: -1. Client opens WebSocket and sends: `{type: 'auth', payload: {token: ''}}` -2. Server responds with: `{type: 'auth.response', status: {code: 200}}` -3. Connection is established after a successful `auth.response`. +1. Client opens WebSocket and sends: `{type: 'auth', data: {token: ''}}` +2. Server responds with: `{type: 'response_event', subtype: 'auth', statusCode: 200}` +3. Connection is established after a successful auth `response_event`. Non-200 status codes are handled as auth failures with automatic retry via exponential backoff. @@ -112,7 +112,7 @@ The default behaviour is to continue to try to connect with an exponential back- | `backoffTimeMax` | `32000` | `MOBIUS_SOCKET_BACKOFF_TIME_MAX` | Maximum milliseconds between connection attempts. | | `backoffTimeReset` | `1000` | `MOBIUS_SOCKET_BACKOFF_TIME_RESET` | Initial milliseconds between connection attempts. | | `forceCloseDelay` | `2000` | `MOBIUS_SOCKET_FORCE_CLOSE_DELAY` | Milliseconds to wait for a close frame before forcing socket closure. | -| `authResponseTimeout` | `10000` | `MOBIUS_SOCKET_AUTH_RESPONSE_TIMEOUT` | Milliseconds to wait for `auth.response` before failing authorization. | +| `wssResponseTimeout` | `10000` | `MOBIUS_SOCKET_RESPONSE_TIMEOUT` | Milliseconds to wait for websocket request/response messages, including auth, before timing out. | | `beforeLogoutOptionsCloseReason` | `done (forced)` | `MOBIUS_SOCKET_LOGOUT_REASON` | Close reason sent on logout. Set to a non-reconnectable reason to prevent reconnect on logout. | ## Contribute diff --git a/packages/@webex/internal-plugin-mobius-socket/src/config.js b/packages/@webex/internal-plugin-mobius-socket/src/config.js index 0f79e3b84f7..30cef0d9e21 100644 --- a/packages/@webex/internal-plugin-mobius-socket/src/config.js +++ b/packages/@webex/internal-plugin-mobius-socket/src/config.js @@ -4,15 +4,13 @@ const mobiusSocketConfig = { /** - * Milliseconds to wait for the auth websocket response before declaring auth failed + * Milliseconds to wait for websocket request/response messages, including auth. * @type {number} */ - authResponseTimeout: process.env.MOBIUS_SOCKET_AUTH_RESPONSE_TIMEOUT || 10000, - /** - * Milliseconds to wait for a request/response style websocket message. - * @type {number} - */ - wssResponseTimeout: process.env.MOBIUS_SOCKET_RESPONSE_TIMEOUT || 10000, + wssResponseTimeout: + process.env.MOBIUS_SOCKET_RESPONSE_TIMEOUT || + process.env.MOBIUS_SOCKET_AUTH_RESPONSE_TIMEOUT || + 10000, /** * Maximum milliseconds between connection attempts * @type {Number} diff --git a/packages/@webex/internal-plugin-mobius-socket/src/mobius-socket.js b/packages/@webex/internal-plugin-mobius-socket/src/mobius-socket.js index f1ba5219240..85d1914b912 100644 --- a/packages/@webex/internal-plugin-mobius-socket/src/mobius-socket.js +++ b/packages/@webex/internal-plugin-mobius-socket/src/mobius-socket.js @@ -28,6 +28,31 @@ function normalizeMobiusAuthToken(token) { return token.replace(/^Bearer\s+/i, ''); } +function normalizeAuthRequestPayload(payload) { + if (!payload || payload.type !== 'auth') { + return payload; + } + + const token = payload?.data?.token || payload?.payload?.token; + + if (typeof token !== 'string') { + return payload; + } + + const normalizedToken = normalizeMobiusAuthToken(token); + const restPayload = {...payload}; + + delete restPayload.payload; + + return { + ...restPayload, + data: { + ...(payload.data || {}), + token: normalizedToken, + }, + }; +} + const MobiusSocket = WebexPlugin.extend({ namespace: 'MobiusSocket', lastError: undefined, @@ -274,23 +299,10 @@ const MobiusSocket = WebexPlugin.extend({ return Promise.reject(new Error(`Mobius socket is not connected for session ${sessionId}`)); } - const normalizedPayload = - payload.type === 'auth' && typeof payload?.payload?.token === 'string' - ? { - ...payload, - payload: { - ...payload.payload, - token: normalizeMobiusAuthToken(payload.payload.token), - }, - } - : payload; + const normalizedPayload = normalizeAuthRequestPayload(payload); return socket.sendRequest(normalizedPayload, { - timeout: - requestOptions.timeout || - this.config.wssResponseTimeout || - this.config.authResponseTimeout || - 10000, + timeout: requestOptions.timeout || this.config.wssResponseTimeout || 10000, matchesResponse: (response, request) => response?.type === 'response_event' && response?.subtype === request.type && @@ -677,7 +689,8 @@ const MobiusSocket = WebexPlugin.extend({ ([webSocketUrl, token]) => { let options = { forceCloseDelay: this.config.forceCloseDelay, - authResponseTimeout: this.config.authResponseTimeout, + wssResponseTimeout: + this.config.wssResponseTimeout || this.config.authResponseTimeout || 10000, token: normalizeMobiusAuthToken(token.toString()), trackingId: `${this.webex.sessionId}_${Date.now()}`, logger: this.logger, diff --git a/packages/@webex/internal-plugin-mobius-socket/src/socket/socket-base.js b/packages/@webex/internal-plugin-mobius-socket/src/socket/socket-base.js index f61a3c3940b..8beef05a5bb 100644 --- a/packages/@webex/internal-plugin-mobius-socket/src/socket/socket-base.js +++ b/packages/@webex/internal-plugin-mobius-socket/src/socket/socket-base.js @@ -368,7 +368,7 @@ export default class Socket extends EventEmitter { const request = {...data}; const trackingId = request.trackingId || this._createTrackingId(); - const timeout = options.timeout || this.authResponseTimeout || 10000; + const timeout = options.timeout || this.wssResponseTimeout || this.authResponseTimeout || 10000; const matchesResponse = options.matchesResponse || ((response) => response?.trackingId === trackingId && response?.type === 'response_event'); @@ -478,7 +478,7 @@ export default class Socket extends EventEmitter { }, }, { - timeout: this.authResponseTimeout || 10000, + timeout: this.wssResponseTimeout || this.authResponseTimeout || 10000, matchesResponse: (response, request) => response?.type === 'response_event' && response?.subtype === MESSAGE_TYPES.AUTH && diff --git a/packages/@webex/internal-plugin-mobius-socket/test/unit/spec/mobius-socket.js b/packages/@webex/internal-plugin-mobius-socket/test/unit/spec/mobius-socket.js index f9b13718e6e..82cc168020e 100644 --- a/packages/@webex/internal-plugin-mobius-socket/test/unit/spec/mobius-socket.js +++ b/packages/@webex/internal-plugin-mobius-socket/test/unit/spec/mobius-socket.js @@ -825,7 +825,7 @@ describe('plugin-mobius-socket', () => { const requestPromise = mobiusSocket.sendWssRequest({ type: 'auth', - payload: { + data: { token: 'Bearer test', }, }); @@ -834,7 +834,7 @@ describe('plugin-mobius-socket', () => { const requestPayload = JSON.parse(mockWebSocket.send.lastCall.args[0]); - assert.equal(requestPayload.payload.token, 'test'); + assert.equal(requestPayload.data.token, 'test'); mockWebSocket.emit('message', { data: JSON.stringify({ @@ -860,7 +860,7 @@ describe('plugin-mobius-socket', () => { const authPayload = JSON.parse(mockWebSocket.send.firstCall.args[0]); assert.equal(authPayload.type, MESSAGE_TYPES.AUTH); - assert.equal(authPayload.payload.token, 'FAKE'); + assert.equal(authPayload.data.token, 'FAKE'); }); it('rejects when a matching response_event is non-2xx', async () => { @@ -868,7 +868,7 @@ describe('plugin-mobius-socket', () => { const requestPromise = mobiusSocket.sendWssRequest({ type: 'auth', - payload: { + data: { token: 'Bearer test', }, }); @@ -900,7 +900,7 @@ describe('plugin-mobius-socket', () => { const requestPromise = mobiusSocket.sendWssRequest({ type: 'auth', - payload: { + data: { token: 'Bearer test', }, }); @@ -920,7 +920,7 @@ describe('plugin-mobius-socket', () => { const requestPromise = mobiusSocket.sendWssRequest({ type: 'auth', - payload: { + data: { token: 'Bearer test', }, }); @@ -936,6 +936,36 @@ describe('plugin-mobius-socket', () => { assert.equal(error.reason, 'service rejected request'); }); }); + + it('converts legacy auth payload token requests to the data envelope', async () => { + await mobiusSocket.connect(); + + const requestPromise = mobiusSocket.sendWssRequest({ + type: 'auth', + payload: { + token: 'Bearer test', + }, + }); + + await promiseTick(); + + const requestPayload = JSON.parse(mockWebSocket.send.lastCall.args[0]); + + assert.equal(requestPayload.data.token, 'test'); + assert.isUndefined(requestPayload.payload); + + mockWebSocket.emit('message', { + data: JSON.stringify({ + type: 'response_event', + subtype: 'auth', + trackingId: requestPayload.trackingId, + statusCode: 200, + statusMessage: 'OK', + }), + }); + + await requestPromise; + }); }); describe('#_emit()', () => { @@ -1574,7 +1604,7 @@ describe('plugin-mobius-socket', () => { assert.isObject(callArgs[1]); assert.equal(callArgs[1].token, 'mock-token'); assert.isDefined(callArgs[1].forceCloseDelay); - assert.isDefined(callArgs[1].authResponseTimeout); + assert.isDefined(callArgs[1].wssResponseTimeout); }); it('should log with correct prefix for normal connection', async () => { @@ -1595,7 +1625,7 @@ describe('plugin-mobius-socket', () => { it('should merge custom mobiusSocket options when provided', async () => { webex.config.defaultMobiusSocketOptions = { customOption: 'test-value', - authResponseTimeout: 99999, + wssResponseTimeout: 99999, }; await mobiusSocket._prepareAndOpenSocket(mockSocket, undefined, false); @@ -1603,7 +1633,7 @@ describe('plugin-mobius-socket', () => { const callArgs = mockSocket.open.firstCall.args; assert.equal(callArgs[1].customOption, 'test-value'); - assert.equal(callArgs[1].authResponseTimeout, 99999); // Custom value overrides default + assert.equal(callArgs[1].wssResponseTimeout, 99999); // Custom value overrides default }); it('should return the webSocketUrl after opening', async () => { diff --git a/packages/@webex/internal-plugin-mobius-socket/test/unit/spec/socket.js b/packages/@webex/internal-plugin-mobius-socket/test/unit/spec/socket.js index 506dc13f7cc..fc642fb9228 100644 --- a/packages/@webex/internal-plugin-mobius-socket/test/unit/spec/socket.js +++ b/packages/@webex/internal-plugin-mobius-socket/test/unit/spec/socket.js @@ -134,7 +134,7 @@ describe('plugin-mobius-socket', () => { it('accepts a logLevelToken option', () => { const promise = socket.open('ws://example.com', { forceCloseDelay: mockoptions.forceCloseDelay, - authResponseTimeout: mockoptions.authResponseTimeout, + wssResponseTimeout: mockoptions.authResponseTimeout, logger: console, token: 'mocktoken', trackingId: 'mocktrackingid', @@ -352,9 +352,9 @@ describe('plugin-mobius-socket', () => { const firstCallArgs = JSON.parse(mockWebSocket.send.firstCall.args[0]); assert.equal(firstCallArgs.type, MESSAGE_TYPES.AUTH); - assert.property(firstCallArgs, 'payload'); - assert.property(firstCallArgs.payload, 'token'); - assert.equal(firstCallArgs.payload.token, 'mocktoken'); + assert.property(firstCallArgs, 'data'); + assert.property(firstCallArgs.data, 'token'); + assert.equal(firstCallArgs.data.token, 'mocktoken'); assert.property(firstCallArgs, 'trackingId'); }); @@ -364,7 +364,7 @@ describe('plugin-mobius-socket', () => { s.open('ws://example.com', { forceCloseDelay: mockoptions.forceCloseDelay, - authResponseTimeout: mockoptions.authResponseTimeout, + wssResponseTimeout: mockoptions.authResponseTimeout, logger: console, token: 'mocktoken', trackingId: 'mocktrackingid', @@ -375,8 +375,8 @@ describe('plugin-mobius-socket', () => { const firstCallArgs = JSON.parse(mockWebSocket.send.firstCall.args[0]); assert.equal(firstCallArgs.type, MESSAGE_TYPES.AUTH); - assert.property(firstCallArgs, 'payload'); - assert.equal(firstCallArgs.payload.token, 'mocktoken'); + assert.property(firstCallArgs, 'data'); + assert.equal(firstCallArgs.data.token, 'mocktoken'); assert.property(firstCallArgs, 'trackingId'); return s.close(); From cdc9b5d54d81f2644d99a5954b4997b7c18a3d15 Mon Sep 17 00:00:00 2001 From: Ravi Chandra Sekhar Sarika Date: Tue, 14 Apr 2026 23:51:08 +0530 Subject: [PATCH 03/10] fix(internal-plugin-mobius-socket): tighten websocket response handling --- .../src/mobius-socket.js | 11 ++---- .../src/socket/socket-base.js | 20 ++++++---- .../test/unit/spec/mobius-socket.js | 37 +++++++++++++++++++ 3 files changed, 53 insertions(+), 15 deletions(-) diff --git a/packages/@webex/internal-plugin-mobius-socket/src/mobius-socket.js b/packages/@webex/internal-plugin-mobius-socket/src/mobius-socket.js index 85d1914b912..cc131965217 100644 --- a/packages/@webex/internal-plugin-mobius-socket/src/mobius-socket.js +++ b/packages/@webex/internal-plugin-mobius-socket/src/mobius-socket.js @@ -280,7 +280,7 @@ const MobiusSocket = WebexPlugin.extend({ * @returns {Promise} */ sendWssRequest(payload, sessionIdOrOptions = this.defaultSessionId, options = {}) { - if (!payload || typeof payload !== 'object') { + if (!payload || typeof payload !== 'object' || Array.isArray(payload)) { return Promise.reject(new Error('`payload` is required')); } @@ -307,10 +307,8 @@ const MobiusSocket = WebexPlugin.extend({ response?.type === 'response_event' && response?.subtype === request.type && response?.trackingId === request.trackingId, - getStatusCode: (response) => - response?.statusCode || response?.status?.code || response?.data?.statusCode, - getStatusMessage: (response) => - response?.statusMessage || response?.status?.message || response?.data?.statusMessage, + getStatusCode: (response) => response?.statusCode, + getStatusMessage: (response) => response?.statusMessage, createError: (response, statusCode, statusMessage) => this._createWssResponseError(response, statusCode, statusMessage), createTimeoutError: (request) => @@ -689,8 +687,7 @@ const MobiusSocket = WebexPlugin.extend({ ([webSocketUrl, token]) => { let options = { forceCloseDelay: this.config.forceCloseDelay, - wssResponseTimeout: - this.config.wssResponseTimeout || this.config.authResponseTimeout || 10000, + wssResponseTimeout: this.config.wssResponseTimeout || 10000, token: normalizeMobiusAuthToken(token.toString()), trackingId: `${this.webex.sessionId}_${Date.now()}`, logger: this.logger, diff --git a/packages/@webex/internal-plugin-mobius-socket/src/socket/socket-base.js b/packages/@webex/internal-plugin-mobius-socket/src/socket/socket-base.js index 8beef05a5bb..ce66507882c 100644 --- a/packages/@webex/internal-plugin-mobius-socket/src/socket/socket-base.js +++ b/packages/@webex/internal-plugin-mobius-socket/src/socket/socket-base.js @@ -372,13 +372,8 @@ export default class Socket extends EventEmitter { const matchesResponse = options.matchesResponse || ((response) => response?.trackingId === trackingId && response?.type === 'response_event'); - const getStatusCode = - options.getStatusCode || - ((response) => response?.statusCode || response?.status?.code || response?.data?.statusCode); - const getStatusMessage = - options.getStatusMessage || - ((response) => - response?.statusMessage || response?.status?.message || response?.data?.statusMessage); + const getStatusCode = options.getStatusCode || ((response) => response?.statusCode); + const getStatusMessage = options.getStatusMessage || ((response) => response?.statusMessage); const createError = options.createError || ((response, statusCode, statusMessage) => @@ -547,6 +542,7 @@ export default class Socket extends EventEmitter { return false; } + // Pending request correlation currently requires trackingId on the response. const pendingResponse = response.trackingId ? this._pendingResponses.get(response.trackingId) : undefined; @@ -562,7 +558,15 @@ export default class Socket extends EventEmitter { const statusCode = pendingResponse.getStatusCode(response); const statusMessage = pendingResponse.getStatusMessage(response); - if (statusCode >= 200 && statusCode < 300) { + if (statusCode === undefined) { + pendingResponse.reject( + pendingResponse.createError( + response, + statusCode, + statusMessage || 'Socket response missing status code' + ) + ); + } else if (statusCode >= 200 && statusCode < 300) { pendingResponse.resolve(response); } else { pendingResponse.reject(pendingResponse.createError(response, statusCode, statusMessage)); diff --git a/packages/@webex/internal-plugin-mobius-socket/test/unit/spec/mobius-socket.js b/packages/@webex/internal-plugin-mobius-socket/test/unit/spec/mobius-socket.js index 82cc168020e..f0bfc719cbc 100644 --- a/packages/@webex/internal-plugin-mobius-socket/test/unit/spec/mobius-socket.js +++ b/packages/@webex/internal-plugin-mobius-socket/test/unit/spec/mobius-socket.js @@ -915,6 +915,35 @@ describe('plugin-mobius-socket', () => { }); }); + it('rejects with a clear error when the matching response is missing status code', async () => { + await mobiusSocket.connect(); + + const requestPromise = mobiusSocket.sendWssRequest({ + type: 'auth', + data: { + token: 'Bearer test', + }, + }); + + await promiseTick(); + + const requestPayload = JSON.parse(mockWebSocket.send.lastCall.args[0]); + + mockWebSocket.emit('message', { + data: JSON.stringify({ + type: 'response_event', + subtype: 'auth', + trackingId: requestPayload.trackingId, + }), + }); + + await assert.isRejected(requestPromise).then((error) => { + assert.equal(error.name, 'MobiusSocketResponseError'); + assert.isUndefined(error.statusCode); + assert.equal(error.statusMessage, 'Socket response missing status code'); + }); + }); + it('rejects pending requests when the active socket closes', async () => { await mobiusSocket.connect(); @@ -966,6 +995,14 @@ describe('plugin-mobius-socket', () => { await requestPromise; }); + + it('rejects array payloads', async () => { + await mobiusSocket.connect(); + + await assert.isRejected(mobiusSocket.sendWssRequest([])).then((error) => { + assert.equal(error.message, '`payload` is required'); + }); + }); }); describe('#_emit()', () => { From df6cfb5302668ba75a54f2f15794b748e743bc02 Mon Sep 17 00:00:00 2001 From: Ravi Chandra Sekhar Sarika Date: Wed, 15 Apr 2026 00:01:04 +0530 Subject: [PATCH 04/10] fix(internal-plugin-mobius-socket): add websocket retry limits --- .../@webex/internal-plugin-mobius-socket/src/config.js | 10 ++++++++++ .../test/unit/spec/mobius-socket.js | 7 +++++++ 2 files changed, 17 insertions(+) diff --git a/packages/@webex/internal-plugin-mobius-socket/src/config.js b/packages/@webex/internal-plugin-mobius-socket/src/config.js index 30cef0d9e21..34ae3718ec6 100644 --- a/packages/@webex/internal-plugin-mobius-socket/src/config.js +++ b/packages/@webex/internal-plugin-mobius-socket/src/config.js @@ -21,6 +21,16 @@ const mobiusSocketConfig = { * @type {Number} */ backoffTimeReset: process.env.MOBIUS_SOCKET_BACKOFF_TIME_RESET || 1000, + /** + * Maximum number of retries for the initial connect() flow before rejecting. + * @type {Number} + */ + initialConnectionMaxRetries: process.env.MOBIUS_SOCKET_INITIAL_CONNECTION_MAX_RETRIES || 2, + /** + * Maximum number of retries for reconnect attempts after the socket has connected once. + * @type {Number} + */ + maxRetries: process.env.MOBIUS_SOCKET_MAX_RETRIES || 0, /** * Milliseconds to wait for a close frame before declaring the socket dead and * discarding it diff --git a/packages/@webex/internal-plugin-mobius-socket/test/unit/spec/mobius-socket.js b/packages/@webex/internal-plugin-mobius-socket/test/unit/spec/mobius-socket.js index f0bfc719cbc..57361146cf0 100644 --- a/packages/@webex/internal-plugin-mobius-socket/test/unit/spec/mobius-socket.js +++ b/packages/@webex/internal-plugin-mobius-socket/test/unit/spec/mobius-socket.js @@ -329,6 +329,13 @@ describe('plugin-mobius-socket', () => { }); }; + // skipping due to apparent bug with lolex in all browsers but Chrome. + skipInBrowser(it)('fails after default `initialConnectionMaxRetries` attempts', () => { + mobiusSocket.config.maxRetries = 0; + + return check(); + }); + // skipping due to apparent bug with lolex in all browsers but Chrome. // if initial retries is zero and mobiusSocket has never connected max retries is used skipInBrowser(it)('fails after `maxRetries` attempts', () => { From ac8c66df3113cf1607a6efdd8b47c3021efdc676 Mon Sep 17 00:00:00 2001 From: Ravi Chandra Sekhar Sarika Date: Wed, 15 Apr 2026 00:47:49 +0530 Subject: [PATCH 05/10] fix: fixed tests --- .../test/unit/spec/mobius-socket-events.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/@webex/internal-plugin-mobius-socket/test/unit/spec/mobius-socket-events.js b/packages/@webex/internal-plugin-mobius-socket/test/unit/spec/mobius-socket-events.js index c0a74c4b6e9..1b887e633a8 100644 --- a/packages/@webex/internal-plugin-mobius-socket/test/unit/spec/mobius-socket-events.js +++ b/packages/@webex/internal-plugin-mobius-socket/test/unit/spec/mobius-socket-events.js @@ -17,7 +17,7 @@ import promiseTick from '../lib/promise-tick'; describe('plugin-mobiusSocket', () => { describe('MobiusSocket', () => { describe('Events', () => { - let clock, mobiusSocket, mockWebSocket, socketOpenStub, webex; + let clock, mobiusSocket, mockWebSocket, originalSendSpy, socketOpenStub, webex; const fakeTestMessage = { id: uuid.v4(), @@ -43,7 +43,8 @@ describe('plugin-mobiusSocket', () => { }; const emitAuthResponse = ({statusCode = 200, statusMessage = 'OK'} = {}) => { - const authRequest = JSON.parse(mockWebSocket.send.lastCall.args[0]); + const sendSpy = mockWebSocket.send.lastCall ? mockWebSocket.send : originalSendSpy; + const authRequest = JSON.parse(sendSpy.lastCall.args[0]); mockWebSocket.emit('message', { data: JSON.stringify({ @@ -95,6 +96,7 @@ describe('plugin-mobiusSocket', () => { webex.logger = console; mockWebSocket = new MockWebSocket('ws://example.com'); + originalSendSpy = mockWebSocket.send; sinon.stub(Socket, 'getWebSocketConstructor').returns(() => mockWebSocket); const origOpen = Socket.prototype.open; From ec5779b467752e477df262342b1f76f1819348a2 Mon Sep 17 00:00:00 2001 From: Ravi Chandra Sekhar Sarika Date: Wed, 15 Apr 2026 00:48:16 +0530 Subject: [PATCH 06/10] fix: updated readme --- packages/@webex/internal-plugin-mobius-socket/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/@webex/internal-plugin-mobius-socket/README.md b/packages/@webex/internal-plugin-mobius-socket/README.md index 537188cca2d..f16692d3400 100644 --- a/packages/@webex/internal-plugin-mobius-socket/README.md +++ b/packages/@webex/internal-plugin-mobius-socket/README.md @@ -105,12 +105,14 @@ webex.init({ ### Retries -The default behaviour is to continue to try to connect with an exponential back-off. This behavior can be adjusted with the following config params: +Initial `connect()` attempts retry with exponential back-off and reject after a limited number of retries by default. Reconnect behavior can still be configured separately. This behavior can be adjusted with the following config params: | Config Key | Default | Env Override | Description | |---|---|---|---| | `backoffTimeMax` | `32000` | `MOBIUS_SOCKET_BACKOFF_TIME_MAX` | Maximum milliseconds between connection attempts. | | `backoffTimeReset` | `1000` | `MOBIUS_SOCKET_BACKOFF_TIME_RESET` | Initial milliseconds between connection attempts. | +| `initialConnectionMaxRetries` | `2` | `MOBIUS_SOCKET_INITIAL_CONNECTION_MAX_RETRIES` | Maximum retries before the initial `connect()` promise rejects. | +| `maxRetries` | `0` | `MOBIUS_SOCKET_MAX_RETRIES` | Maximum retries for reconnect attempts after the socket has connected once. | | `forceCloseDelay` | `2000` | `MOBIUS_SOCKET_FORCE_CLOSE_DELAY` | Milliseconds to wait for a close frame before forcing socket closure. | | `wssResponseTimeout` | `10000` | `MOBIUS_SOCKET_RESPONSE_TIMEOUT` | Milliseconds to wait for websocket request/response messages, including auth, before timing out. | | `beforeLogoutOptionsCloseReason` | `done (forced)` | `MOBIUS_SOCKET_LOGOUT_REASON` | Close reason sent on logout. Set to a non-reconnectable reason to prevent reconnect on logout. | From 90839c51c7c82c728bbc54f8b7531ce916152a69 Mon Sep 17 00:00:00 2001 From: Ravi Chandra Sekhar Sarika Date: Wed, 15 Apr 2026 00:56:02 +0530 Subject: [PATCH 07/10] refactor(internal-plugin-mobius-socket): keep websocket requests generic --- .../src/mobius-socket.js | 29 +------------ .../test/unit/spec/mobius-socket.js | 41 +++---------------- 2 files changed, 6 insertions(+), 64 deletions(-) diff --git a/packages/@webex/internal-plugin-mobius-socket/src/mobius-socket.js b/packages/@webex/internal-plugin-mobius-socket/src/mobius-socket.js index cc131965217..1dd6e9bbd67 100644 --- a/packages/@webex/internal-plugin-mobius-socket/src/mobius-socket.js +++ b/packages/@webex/internal-plugin-mobius-socket/src/mobius-socket.js @@ -28,31 +28,6 @@ function normalizeMobiusAuthToken(token) { return token.replace(/^Bearer\s+/i, ''); } -function normalizeAuthRequestPayload(payload) { - if (!payload || payload.type !== 'auth') { - return payload; - } - - const token = payload?.data?.token || payload?.payload?.token; - - if (typeof token !== 'string') { - return payload; - } - - const normalizedToken = normalizeMobiusAuthToken(token); - const restPayload = {...payload}; - - delete restPayload.payload; - - return { - ...restPayload, - data: { - ...(payload.data || {}), - token: normalizedToken, - }, - }; -} - const MobiusSocket = WebexPlugin.extend({ namespace: 'MobiusSocket', lastError: undefined, @@ -299,9 +274,7 @@ const MobiusSocket = WebexPlugin.extend({ return Promise.reject(new Error(`Mobius socket is not connected for session ${sessionId}`)); } - const normalizedPayload = normalizeAuthRequestPayload(payload); - - return socket.sendRequest(normalizedPayload, { + return socket.sendRequest(payload, { timeout: requestOptions.timeout || this.config.wssResponseTimeout || 10000, matchesResponse: (response, request) => response?.type === 'response_event' && diff --git a/packages/@webex/internal-plugin-mobius-socket/test/unit/spec/mobius-socket.js b/packages/@webex/internal-plugin-mobius-socket/test/unit/spec/mobius-socket.js index 57361146cf0..885b5d38dec 100644 --- a/packages/@webex/internal-plugin-mobius-socket/test/unit/spec/mobius-socket.js +++ b/packages/@webex/internal-plugin-mobius-socket/test/unit/spec/mobius-socket.js @@ -833,14 +833,13 @@ describe('plugin-mobius-socket', () => { const requestPromise = mobiusSocket.sendWssRequest({ type: 'auth', data: { - token: 'Bearer test', + token: 'test', }, }); await promiseTick(); const requestPayload = JSON.parse(mockWebSocket.send.lastCall.args[0]); - assert.equal(requestPayload.data.token, 'test'); mockWebSocket.emit('message', { @@ -876,7 +875,7 @@ describe('plugin-mobius-socket', () => { const requestPromise = mobiusSocket.sendWssRequest({ type: 'auth', data: { - token: 'Bearer test', + token: 'test', }, }); @@ -908,7 +907,7 @@ describe('plugin-mobius-socket', () => { const requestPromise = mobiusSocket.sendWssRequest({ type: 'auth', data: { - token: 'Bearer test', + token: 'test', }, }); @@ -928,7 +927,7 @@ describe('plugin-mobius-socket', () => { const requestPromise = mobiusSocket.sendWssRequest({ type: 'auth', data: { - token: 'Bearer test', + token: 'test', }, }); @@ -957,7 +956,7 @@ describe('plugin-mobius-socket', () => { const requestPromise = mobiusSocket.sendWssRequest({ type: 'auth', data: { - token: 'Bearer test', + token: 'test', }, }); @@ -973,36 +972,6 @@ describe('plugin-mobius-socket', () => { }); }); - it('converts legacy auth payload token requests to the data envelope', async () => { - await mobiusSocket.connect(); - - const requestPromise = mobiusSocket.sendWssRequest({ - type: 'auth', - payload: { - token: 'Bearer test', - }, - }); - - await promiseTick(); - - const requestPayload = JSON.parse(mockWebSocket.send.lastCall.args[0]); - - assert.equal(requestPayload.data.token, 'test'); - assert.isUndefined(requestPayload.payload); - - mockWebSocket.emit('message', { - data: JSON.stringify({ - type: 'response_event', - subtype: 'auth', - trackingId: requestPayload.trackingId, - statusCode: 200, - statusMessage: 'OK', - }), - }); - - await requestPromise; - }); - it('rejects array payloads', async () => { await mobiusSocket.connect(); From b5302042a11464c44ef43c44ffc8372eb9610b34 Mon Sep 17 00:00:00 2001 From: Ravi Chandra Sekhar Sarika Date: Wed, 15 Apr 2026 01:11:22 +0530 Subject: [PATCH 08/10] refactor(internal-plugin-mobius-socket): simplify websocket timeouts --- .../internal-plugin-mobius-socket/README.md | 2 +- .../src/config.js | 1 + .../src/mobius-socket.js | 4 +- .../src/socket/constants.js | 1 - .../src/socket/socket-base.js | 5 +- .../test/unit/spec/mobius-socket.js | 73 ++++++++++--------- .../test/unit/spec/socket.js | 4 +- 7 files changed, 46 insertions(+), 44 deletions(-) diff --git a/packages/@webex/internal-plugin-mobius-socket/README.md b/packages/@webex/internal-plugin-mobius-socket/README.md index f16692d3400..281f6885410 100644 --- a/packages/@webex/internal-plugin-mobius-socket/README.md +++ b/packages/@webex/internal-plugin-mobius-socket/README.md @@ -112,7 +112,7 @@ Initial `connect()` attempts retry with exponential back-off and reject after a | `backoffTimeMax` | `32000` | `MOBIUS_SOCKET_BACKOFF_TIME_MAX` | Maximum milliseconds between connection attempts. | | `backoffTimeReset` | `1000` | `MOBIUS_SOCKET_BACKOFF_TIME_RESET` | Initial milliseconds between connection attempts. | | `initialConnectionMaxRetries` | `2` | `MOBIUS_SOCKET_INITIAL_CONNECTION_MAX_RETRIES` | Maximum retries before the initial `connect()` promise rejects. | -| `maxRetries` | `0` | `MOBIUS_SOCKET_MAX_RETRIES` | Maximum retries for reconnect attempts after the socket has connected once. | +| `maxRetries` | `0` | `MOBIUS_SOCKET_MAX_RETRIES` | Maximum retries for reconnect attempts after the socket has connected once. `0` means unlimited retries. | | `forceCloseDelay` | `2000` | `MOBIUS_SOCKET_FORCE_CLOSE_DELAY` | Milliseconds to wait for a close frame before forcing socket closure. | | `wssResponseTimeout` | `10000` | `MOBIUS_SOCKET_RESPONSE_TIMEOUT` | Milliseconds to wait for websocket request/response messages, including auth, before timing out. | | `beforeLogoutOptionsCloseReason` | `done (forced)` | `MOBIUS_SOCKET_LOGOUT_REASON` | Close reason sent on logout. Set to a non-reconnectable reason to prevent reconnect on logout. | diff --git a/packages/@webex/internal-plugin-mobius-socket/src/config.js b/packages/@webex/internal-plugin-mobius-socket/src/config.js index 34ae3718ec6..352f4f073cd 100644 --- a/packages/@webex/internal-plugin-mobius-socket/src/config.js +++ b/packages/@webex/internal-plugin-mobius-socket/src/config.js @@ -28,6 +28,7 @@ const mobiusSocketConfig = { initialConnectionMaxRetries: process.env.MOBIUS_SOCKET_INITIAL_CONNECTION_MAX_RETRIES || 2, /** * Maximum number of retries for reconnect attempts after the socket has connected once. + * A value of 0 means unlimited reconnect retries. * @type {Number} */ maxRetries: process.env.MOBIUS_SOCKET_MAX_RETRIES || 0, diff --git a/packages/@webex/internal-plugin-mobius-socket/src/mobius-socket.js b/packages/@webex/internal-plugin-mobius-socket/src/mobius-socket.js index 1dd6e9bbd67..bc10125d1bf 100644 --- a/packages/@webex/internal-plugin-mobius-socket/src/mobius-socket.js +++ b/packages/@webex/internal-plugin-mobius-socket/src/mobius-socket.js @@ -275,7 +275,7 @@ const MobiusSocket = WebexPlugin.extend({ } return socket.sendRequest(payload, { - timeout: requestOptions.timeout || this.config.wssResponseTimeout || 10000, + timeout: requestOptions.timeout, matchesResponse: (response, request) => response?.type === 'response_event' && response?.subtype === request.type && @@ -660,7 +660,7 @@ const MobiusSocket = WebexPlugin.extend({ ([webSocketUrl, token]) => { let options = { forceCloseDelay: this.config.forceCloseDelay, - wssResponseTimeout: this.config.wssResponseTimeout || 10000, + wssResponseTimeout: this.config.wssResponseTimeout, token: normalizeMobiusAuthToken(token.toString()), trackingId: `${this.webex.sessionId}_${Date.now()}`, logger: this.logger, diff --git a/packages/@webex/internal-plugin-mobius-socket/src/socket/constants.js b/packages/@webex/internal-plugin-mobius-socket/src/socket/constants.js index 884dedec081..36067a1bbb3 100644 --- a/packages/@webex/internal-plugin-mobius-socket/src/socket/constants.js +++ b/packages/@webex/internal-plugin-mobius-socket/src/socket/constants.js @@ -7,6 +7,5 @@ export const SOCKET_READY_STATE = Object.freeze({ export const MESSAGE_TYPES = Object.freeze({ AUTH: 'auth', - AUTH_RESPONSE: 'auth.response', EVENT_ACK: 'event_ack', }); diff --git a/packages/@webex/internal-plugin-mobius-socket/src/socket/socket-base.js b/packages/@webex/internal-plugin-mobius-socket/src/socket/socket-base.js index ce66507882c..466811934d0 100644 --- a/packages/@webex/internal-plugin-mobius-socket/src/socket/socket-base.js +++ b/packages/@webex/internal-plugin-mobius-socket/src/socket/socket-base.js @@ -318,6 +318,8 @@ export default class Socket extends EventEmitter { }); } + // Match pending request/response promises before emitting the public message event. + // The message is still emitted afterward for any external listeners that care about it. this._handlePendingResponse(data); this.emit('message', processedEvent); } catch (error) { @@ -368,7 +370,7 @@ export default class Socket extends EventEmitter { const request = {...data}; const trackingId = request.trackingId || this._createTrackingId(); - const timeout = options.timeout || this.wssResponseTimeout || this.authResponseTimeout || 10000; + const timeout = options.timeout || this.wssResponseTimeout || 10000; const matchesResponse = options.matchesResponse || ((response) => response?.trackingId === trackingId && response?.type === 'response_event'); @@ -473,7 +475,6 @@ export default class Socket extends EventEmitter { }, }, { - timeout: this.wssResponseTimeout || this.authResponseTimeout || 10000, matchesResponse: (response, request) => response?.type === 'response_event' && response?.subtype === MESSAGE_TYPES.AUTH && diff --git a/packages/@webex/internal-plugin-mobius-socket/test/unit/spec/mobius-socket.js b/packages/@webex/internal-plugin-mobius-socket/test/unit/spec/mobius-socket.js index 885b5d38dec..2d1d49cc65e 100644 --- a/packages/@webex/internal-plugin-mobius-socket/test/unit/spec/mobius-socket.js +++ b/packages/@webex/internal-plugin-mobius-socket/test/unit/spec/mobius-socket.js @@ -749,7 +749,7 @@ describe('plugin-mobius-socket', () => { const promise = mobiusSocket.connect(); // Wait for the connect call to setup - return promiseTick(webex.internal.mobiusSocket.config.backoffTimeReset).then(() => { + return promiseTick(webex.internal.mobiusSocket.config.backoffTimeReset).then(async () => { // By this time backoffCall and mobiusSocket socket should be defined by the // 'connect' call assert.isDefined(mobiusSocket.backoffCalls.get('mobius-websocket-session'), 'MobiusSocket backoffCall is not defined'); @@ -761,10 +761,9 @@ describe('plugin-mobius-socket', () => { // The socket will never be unset (which seems bad) assert.isDefined(mobiusSocket.socket, 'MobiusSocket socket is not defined'); - return assert.isRejected(promise).then((error) => { - // connection did not fail, so no last error - assert.isUndefined(mobiusSocket.getLastError()); - }); + await assert.isRejected(promise); + // connection did not fail, so no last error + assert.isUndefined(mobiusSocket.getLastError()); }); }); @@ -805,18 +804,20 @@ describe('plugin-mobius-socket', () => { const promise = mobiusSocket.connect(); // Wait for the connect call to setup - return promiseTick(webex.internal.mobiusSocket.config.backoffTimeReset).then(() => { + return promiseTick(webex.internal.mobiusSocket.config.backoffTimeReset).then(async () => { // Calling disconnect will abort the backoffCall, close the socket, and // reject the connect mobiusSocket.disconnect(); - return assert.isRejected(promise).then((error) => { - const lastError = mobiusSocket.getLastError(); + const error = await assert.isRejected(promise); + const lastError = mobiusSocket.getLastError(); - assert.equal(error.message, `MobiusSocket Connection Aborted for ${mobiusSocket.defaultSessionId}`); - assert.isDefined(lastError); - assert.equal(lastError, realError); - }); + assert.equal( + error.message, + `MobiusSocket Connection Aborted for ${mobiusSocket.defaultSessionId}` + ); + assert.isDefined(lastError); + assert.equal(lastError, realError); }); }); }); @@ -893,12 +894,12 @@ describe('plugin-mobius-socket', () => { }), }); - await assert.isRejected(requestPromise).then((error) => { - assert.equal(error.name, 'MobiusSocketResponseError'); - assert.equal(error.statusCode, 403); - assert.equal(error.statusMessage, 'Forbidden'); - assert.equal(error.trackingId, requestPayload.trackingId); - }); + const error = await assert.isRejected(requestPromise); + + assert.equal(error.name, 'MobiusSocketResponseError'); + assert.equal(error.statusCode, 403); + assert.equal(error.statusMessage, 'Forbidden'); + assert.equal(error.trackingId, requestPayload.trackingId); }); it('rejects when the matching response does not arrive before timeout', async () => { @@ -914,11 +915,11 @@ describe('plugin-mobius-socket', () => { clock.tick(101); await promiseTick(); - await assert.isRejected(requestPromise).then((error) => { - assert.equal(error.name, 'MobiusSocketResponseError'); - assert.equal(error.statusCode, 408); - assert.equal(error.statusMessage, 'Mobius websocket response timed out'); - }); + const error = await assert.isRejected(requestPromise); + + assert.equal(error.name, 'MobiusSocketResponseError'); + assert.equal(error.statusCode, 408); + assert.equal(error.statusMessage, 'Mobius websocket response timed out'); }); it('rejects with a clear error when the matching response is missing status code', async () => { @@ -943,11 +944,11 @@ describe('plugin-mobius-socket', () => { }), }); - await assert.isRejected(requestPromise).then((error) => { - assert.equal(error.name, 'MobiusSocketResponseError'); - assert.isUndefined(error.statusCode); - assert.equal(error.statusMessage, 'Socket response missing status code'); - }); + const error = await assert.isRejected(requestPromise); + + assert.equal(error.name, 'MobiusSocketResponseError'); + assert.isUndefined(error.statusCode); + assert.equal(error.statusMessage, 'Socket response missing status code'); }); it('rejects pending requests when the active socket closes', async () => { @@ -965,19 +966,19 @@ describe('plugin-mobius-socket', () => { reason: 'service rejected request', }); - await assert.isRejected(requestPromise).then((error) => { - assert.instanceOf(error, ConnectionError); - assert.equal(error.code, 1003); - assert.equal(error.reason, 'service rejected request'); - }); + const error = await assert.isRejected(requestPromise); + + assert.instanceOf(error, ConnectionError); + assert.equal(error.code, 1003); + assert.equal(error.reason, 'service rejected request'); }); it('rejects array payloads', async () => { await mobiusSocket.connect(); - await assert.isRejected(mobiusSocket.sendWssRequest([])).then((error) => { - assert.equal(error.message, '`payload` is required'); - }); + const error = await assert.isRejected(mobiusSocket.sendWssRequest([])); + + assert.equal(error.message, '`payload` is required'); }); }); diff --git a/packages/@webex/internal-plugin-mobius-socket/test/unit/spec/socket.js b/packages/@webex/internal-plugin-mobius-socket/test/unit/spec/socket.js index fc642fb9228..9a6c127ef21 100644 --- a/packages/@webex/internal-plugin-mobius-socket/test/unit/spec/socket.js +++ b/packages/@webex/internal-plugin-mobius-socket/test/unit/spec/socket.js @@ -134,7 +134,7 @@ describe('plugin-mobius-socket', () => { it('accepts a logLevelToken option', () => { const promise = socket.open('ws://example.com', { forceCloseDelay: mockoptions.forceCloseDelay, - wssResponseTimeout: mockoptions.authResponseTimeout, + wssResponseTimeout: mockoptions.wssResponseTimeout, logger: console, token: 'mocktoken', trackingId: 'mocktrackingid', @@ -364,7 +364,7 @@ describe('plugin-mobius-socket', () => { s.open('ws://example.com', { forceCloseDelay: mockoptions.forceCloseDelay, - wssResponseTimeout: mockoptions.authResponseTimeout, + wssResponseTimeout: mockoptions.wssResponseTimeout, logger: console, token: 'mocktoken', trackingId: 'mocktrackingid', From f4f858216ae0d95585b59c6cefb390806dbd8c1b Mon Sep 17 00:00:00 2001 From: Priya Date: Fri, 17 Apr 2026 01:05:51 +0530 Subject: [PATCH 09/10] feat(calling): add token refresh logic --- .../src/mobius-socket.js | 90 ++++++++++++++++++- .../src/socket/socket-base.js | 87 ++++++++++++++++-- 2 files changed, 170 insertions(+), 7 deletions(-) diff --git a/packages/@webex/internal-plugin-mobius-socket/src/mobius-socket.js b/packages/@webex/internal-plugin-mobius-socket/src/mobius-socket.js index af1de5baaa2..ad859382a29 100644 --- a/packages/@webex/internal-plugin-mobius-socket/src/mobius-socket.js +++ b/packages/@webex/internal-plugin-mobius-socket/src/mobius-socket.js @@ -19,6 +19,8 @@ import { const normalReconnectReasons = ['idle', 'done (forced)']; const DEFAULT_MOBIUS_WEBSOCKET_SESSION = 'mobius-websocket-session'; +const TOKEN_REFRESH_INTERVAL_MS = 1 * 60 * 60 * 1000; // 1 hour +const TEST_MOBIUS_WEBSOCKET_URL = 'wss://mobius.aload-calling1.ciscospark.com/v1/calling/web'; const MobiusSocket = WebexPlugin.extend({ namespace: 'MobiusSocket', @@ -67,6 +69,9 @@ const MobiusSocket = WebexPlugin.extend({ }, initialize() { + this._tokenRefreshTimer = undefined; + this._tokenRefreshInFlight = undefined; + /* When one of these legacy feature gets updated, this event would be triggered * group-message-notifications @@ -379,6 +384,12 @@ const MobiusSocket = WebexPlugin.extend({ // Update overall connected status this.connected = this.hasConnectedSockets(); + const hasConnectedSocket = Array.from(this.sockets.values()).some( + (socket) => socket?.connected + ); + if (!hasConnectedSocket) { + this._stopTokenRefreshTimer(); + } }); }, @@ -398,6 +409,7 @@ const MobiusSocket = WebexPlugin.extend({ this.connected = false; this.sockets.clear(); this.backoffCalls.clear(); + this._stopTokenRefreshTimer(); // Clear connection promises to prevent stale promises if (this._connectPromises) { this._connectPromises.clear(); @@ -434,7 +446,11 @@ const MobiusSocket = WebexPlugin.extend({ _prepareUrl(webSocketUrl) { if (!webSocketUrl) { - webSocketUrl = this.webex.internal.device.webSocketUrl; + // Original catalog-based URL resolution (kept for later restore): + // webSocketUrl = this.webex.internal.device.webSocketUrl; + + // Temporary testing override until catalog URI is available. + webSocketUrl = TEST_MOBIUS_WEBSOCKET_URL; } // TODO: Validate the host against the service catalog @@ -655,6 +671,7 @@ const MobiusSocket = WebexPlugin.extend({ this.connecting = this.hasConnectingSockets(); this.connected = this.hasConnectedSockets(); this.hasEverConnected = true; + this._startTokenRefreshTimer(); this._emit(sid, 'online'); } @@ -793,6 +810,71 @@ const MobiusSocket = WebexPlugin.extend({ return handlers; }, + _startTokenRefreshTimer() { + const hasConnectedSocket = Array.from(this.sockets.values()).some( + (socket) => socket?.connected + ); + if (this._tokenRefreshTimer || !hasConnectedSocket) { + return; + } + + this._tokenRefreshTimer = setInterval(() => { + this._refreshAndReauthSockets().catch((error) => { + this.logger.error(`${this.namespace}: periodic token refresh failed`, error); + }); + }, TOKEN_REFRESH_INTERVAL_MS); + }, + + _stopTokenRefreshTimer() { + if (!this._tokenRefreshTimer) { + return; + } + + clearInterval(this._tokenRefreshTimer); + this._tokenRefreshTimer = undefined; + }, + + _refreshAndReauthSockets() { + if (this._tokenRefreshInFlight) { + return this._tokenRefreshInFlight; + } + + const hasConnectedSocket = Array.from(this.sockets.values()).some( + (socket) => socket?.connected + ); + if (!hasConnectedSocket) { + this._stopTokenRefreshTimer(); + + return Promise.resolve(); + } + + this._tokenRefreshInFlight = this.webex.credentials + .refresh({force: true}) + .then(() => this.webex.credentials.getUserToken()) + .then((token) => { + const refreshedToken = + token && typeof token.toString === 'function' ? token.toString() : token; + const authPayloadPromises = []; + + for (const socket of this.sockets.values()) { + if (socket?.connected) { + authPayloadPromises.push(socket.refresh(refreshedToken)); + } + } + + return Promise.all(authPayloadPromises); + }) + .catch((error) => { + this.logger.error(`${this.namespace}: failed to refresh/re-auth Mobius sockets`, error); + throw error; + }) + .finally(() => { + this._tokenRefreshInFlight = undefined; + }); + + return this._tokenRefreshInFlight; + }, + _onclose(sessionId, event, sourceSocket) { // I don't see any way to avoid the complexity or statement count in here. /* eslint complexity: [0] */ @@ -819,6 +901,12 @@ const MobiusSocket = WebexPlugin.extend({ // Update overall connected status this.connecting = this.hasConnectingSockets(); this.connected = this.hasConnectedSockets(); + const hasConnectedSocketAfterClose = Array.from(this.sockets.values()).some( + (socket) => socket?.connected + ); + if (!hasConnectedSocketAfterClose) { + this._stopTokenRefreshTimer(); + } } else { // Old socket closed; do not flip connection state this.logger.info( diff --git a/packages/@webex/internal-plugin-mobius-socket/src/socket/socket-base.js b/packages/@webex/internal-plugin-mobius-socket/src/socket/socket-base.js index 729056900ef..6380390f558 100644 --- a/packages/@webex/internal-plugin-mobius-socket/src/socket/socket-base.js +++ b/packages/@webex/internal-plugin-mobius-socket/src/socket/socket-base.js @@ -378,7 +378,80 @@ export default class Socket extends EventEmitter { } /** - * Sends an auth message up the socket + * Sends an auth message up the socket with a refreshed token. + * @param {string} token + * @returns {Promise} + */ + refresh(token) { + return new Promise((resolve, reject) => { + if (!token) { + reject(new Error('`token` is required for Socket#refresh()')); + + return; + } + + const refreshedToken = + token && typeof token.toString === 'function' ? token.toString() : token; + + this.token = refreshedToken; + this.logger.info(`socket,${this._domain}: authorizing`); + let authResponseTimer; + + const cleanup = (handler) => { + clearTimeout(authResponseTimer); + if (handler) { + this.off('message', handler); + } + }; + + const waitForAuthResponse = (event) => { + if (event.data?.type !== MESSAGE_TYPES.AUTH_RESPONSE) { + return; + } + + cleanup(waitForAuthResponse); + + const statusCode = event.data?.status?.code; + + if (statusCode >= 200 && statusCode < 300) { + resolve(); + + return; + } + + reject( + new NotAuthorized({ + code: statusCode, + reason: event.data?.status?.message || 'Mobius auth failed', + }) + ); + }; + + this.on('message', waitForAuthResponse); + authResponseTimer = safeSetTimeout(() => { + cleanup(waitForAuthResponse); + reject( + new NotAuthorized({ + reason: 'Mobius auth response not received before timeout', + }) + ); + }, this.authResponseTimeout || 10000); + + this.send({ + type: MESSAGE_TYPES.AUTH, + trackingId: this._createTrackingId(), + payload: { + token: refreshedToken, + }, + }).catch((error) => { + cleanup(waitForAuthResponse); + reject(error); + }); + }); + } + + /** + * Sends an initial auth message up the socket * @private * @returns {Promise} */ @@ -387,9 +460,11 @@ export default class Socket extends EventEmitter { this.logger.info(`socket,${this._domain}: authorizing`); let authResponseTimer; - const cleanup = () => { + const cleanup = (handler) => { clearTimeout(authResponseTimer); - this.off('message', waitForAuthResponse); + if (handler) { + this.off('message', handler); + } }; const waitForAuthResponse = (event) => { @@ -397,7 +472,7 @@ export default class Socket extends EventEmitter { return; } - cleanup(); + cleanup(waitForAuthResponse); const statusCode = event.data?.status?.code; @@ -417,7 +492,7 @@ export default class Socket extends EventEmitter { this.on('message', waitForAuthResponse); authResponseTimer = safeSetTimeout(() => { - cleanup(); + cleanup(waitForAuthResponse); reject( new NotAuthorized({ reason: 'Mobius auth response not received before timeout', @@ -432,7 +507,7 @@ export default class Socket extends EventEmitter { token: this.token, }, }).catch((error) => { - cleanup(); + cleanup(waitForAuthResponse); reject(error); }); }); From 0dcc5965225764120e9103e3f1c39c9f8928a758 Mon Sep 17 00:00:00 2001 From: Priya Date: Mon, 20 Apr 2026 02:15:16 +0530 Subject: [PATCH 10/10] feat(calling): final changes --- .../src/mobius-socket.js | 41 +++++++++---------- .../src/socket/socket-base.js | 12 ++++++ 2 files changed, 31 insertions(+), 22 deletions(-) diff --git a/packages/@webex/internal-plugin-mobius-socket/src/mobius-socket.js b/packages/@webex/internal-plugin-mobius-socket/src/mobius-socket.js index c1cf9a92ed8..c21cd622c82 100644 --- a/packages/@webex/internal-plugin-mobius-socket/src/mobius-socket.js +++ b/packages/@webex/internal-plugin-mobius-socket/src/mobius-socket.js @@ -312,13 +312,21 @@ const MobiusSocket = WebexPlugin.extend({ /** * Check if a socket is connected - * @param {string} [sessionId=this.defaultSessionId] - The session identifier + * @param {string} [sessionId] - Optional session identifier * @returns {boolean|undefined} True if the socket is connected */ - hasConnectedSockets(sessionId = this.defaultSessionId) { - const socket = this.sockets.get(sessionId || this.defaultSessionId); + hasConnectedSockets(sessionId) { + if (sessionId) { + return this.sockets.get(sessionId)?.connected; + } + + for (const socket of this.sockets.values()) { + if (socket?.connected) { + return true; + } + } - return socket?.connected; + return false; }, /** @@ -442,10 +450,7 @@ const MobiusSocket = WebexPlugin.extend({ // Update overall connected status this.connected = this.hasConnectedSockets(); - const hasConnectedSocket = Array.from(this.sockets.values()).some( - (socket) => socket?.connected - ); - if (!hasConnectedSocket) { + if (!this.hasConnectedSockets()) { this._stopTokenRefreshTimer(); } }); @@ -678,6 +683,7 @@ const MobiusSocket = WebexPlugin.extend({ forceCloseDelay: this.config.forceCloseDelay, wssResponseTimeout: this.config.wssResponseTimeout, token: normalizeMobiusAuthToken(token.toString()), + refreshToken: () => this._refreshToken(), trackingId: `${this.webex.sessionId}_${Date.now()}`, logger: this.logger, }; @@ -883,15 +889,12 @@ const MobiusSocket = WebexPlugin.extend({ }, _startTokenRefreshTimer() { - const hasConnectedSocket = Array.from(this.sockets.values()).some( - (socket) => socket?.connected - ); - if (this._tokenRefreshTimer || !hasConnectedSocket) { + if (this._tokenRefreshTimer || !this.hasConnectedSockets()) { return; } this._tokenRefreshTimer = setInterval(() => { - this._refreshAndReauthSockets().catch((error) => { + this._refreshToken().catch((error) => { this.logger.error(`${this.namespace}: periodic token refresh failed`, error); }); }, TOKEN_REFRESH_INTERVAL_MS); @@ -906,15 +909,12 @@ const MobiusSocket = WebexPlugin.extend({ this._tokenRefreshTimer = undefined; }, - _refreshAndReauthSockets() { + _refreshToken() { if (this._tokenRefreshInFlight) { return this._tokenRefreshInFlight; } - const hasConnectedSocket = Array.from(this.sockets.values()).some( - (socket) => socket?.connected - ); - if (!hasConnectedSocket) { + if (!this.hasConnectedSockets()) { this._stopTokenRefreshTimer(); return Promise.resolve(); @@ -979,10 +979,7 @@ const MobiusSocket = WebexPlugin.extend({ // Update overall connected status this.connecting = this.hasConnectingSockets(); this.connected = this.hasConnectedSockets(); - const hasConnectedSocketAfterClose = Array.from(this.sockets.values()).some( - (socket) => socket?.connected - ); - if (!hasConnectedSocketAfterClose) { + if (!this.hasConnectedSockets()) { this._stopTokenRefreshTimer(); } } else { diff --git a/packages/@webex/internal-plugin-mobius-socket/src/socket/socket-base.js b/packages/@webex/internal-plugin-mobius-socket/src/socket/socket-base.js index 8d6d8a236cc..a2e8f399fcf 100644 --- a/packages/@webex/internal-plugin-mobius-socket/src/socket/socket-base.js +++ b/packages/@webex/internal-plugin-mobius-socket/src/socket/socket-base.js @@ -569,6 +569,18 @@ export default class Socket extends EventEmitter { const statusCode = pendingResponse.getStatusCode(response); const statusMessage = pendingResponse.getStatusMessage(response); + if (statusCode === 440 && response?.subtype !== MESSAGE_TYPES.AUTH) { + if (typeof this.refreshToken === 'function') { + Promise.resolve(this.refreshToken(response)).catch((error) => { + this.logger.warn(`socket,${this._domain}: failed token-expiry re-auth`, error); + }); + } else { + this.logger.warn( + `socket,${this._domain}: refreshToken callback is unavailable for statusCode 440` + ); + } + } + if (statusCode === undefined) { pendingResponse.reject( pendingResponse.createError(