Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(CC): add oauth login to samples #4022

Merged
merged 7 commits into from
Dec 16, 2024
Merged

feat(CC): add oauth login to samples #4022

merged 7 commits into from
Dec 16, 2024

Conversation

bhabalan
Copy link
Contributor

@bhabalan bhabalan commented Dec 6, 2024

COMPLETES #SPARK-583528

This pull request addresses

Need to do all testing with an integration that has minimal scopes instead of copying from the dev portal or agent desktop

by making the following changes

Added oauth login to cc samples

Vidcast: https://app.vidcast.io/share/86fafaad-afd3-4218-b885-78aa90ca34a3

Change Type

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • Tooling change
  • Internal code refactor

The following scenarios were tested

  • Login via OAuth
  • Initialize webex
  • Register CC
  • Station login via extension
  • List teams
  • Set agent status to idle / available
  • List buddy agents
  • Accept a task on device
  • Observe that hold/resume works

Scopes used:

ccMandatoryScopes = [
    "cjp:config_read",
    "cjp:config_write",
    "cjp:config",
    "cjp:user",
  ];

const webRTCCallingScopes = [
    "spark:webrtc_calling",
    "spark:calls_read",
    "spark:calls_write",
    "spark:xsi"
  ];
  
const additionalScopes = [
    "spark:kms", // to avoid token downscope to only spark:kms error on SDK init
  ];

The integration was created using the credentials listed in https://confluence-eng-gpk2.cisco.com/conf/x/C86lIw
More details about the discovery are listed in the same confluence

I certified that

  • I have read and followed contributing guidelines

  • I discussed changes with code owners prior to submitting this pull request

  • I have not skipped any automated checks

  • All existing and new tests passed

  • I have updated the documentation accordingly


Make sure to have followed the contributing guidelines before submitting.

Summary by CodeRabbit

  • New Features

    • Introduced OAuth authentication capabilities alongside existing access token methods in the contact center application.
    • Added a sample interface for the Webex JavaScript SDK, including sections for authentication, registration, and agent management.
  • Bug Fixes

    • Enhanced error handling in the buddy agent retrieval process.
  • Documentation

    • Updated HTML structure for improved user navigation and interaction with the Webex SDK functionalities.

@bhabalan bhabalan added the validated If the pull request is validated for automation. label Dec 6, 2024
@bhabalan bhabalan self-assigned this Dec 6, 2024
@bhabalan bhabalan requested review from a team as code owners December 6, 2024 09:12
Copy link

coderabbitai bot commented Dec 6, 2024

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

The changes introduced in this pull request enhance the contact center application by implementing OAuth authentication alongside the existing access token method. The modifications include the addition of new UI elements and functions for managing the OAuth process, as well as a new HTML file that serves as a sample interface for the Webex JavaScript SDK. The existing functionality for access tokens remains intact, ensuring a seamless integration of the new authentication method.

Changes

File Path Change Summary
docs/samples/contact-center/app.js - Added OAuth authentication capabilities with new variables oauthFormElm and oauthStatusElm.
- Introduced initOauth function to handle OAuth setup and login process.
- Modified changeAuthType to include OAuth option.
- Added check for existing OAuth session in localStorage.
docs/samples/contact-center/index.html - Created a new sample HTML interface for the Webex JavaScript SDK, including sections for authentication, registration, and agent management.
- Implemented interactive elements for user navigation and authentication status display.

Possibly related PRs

Suggested reviewers

  • maxinteger

🐇 In the contact center, we now can say,
With OAuth in place, we’ll log in today!
A form to fill, a status to see,
Authentication made easy, oh what glee!
With buttons to press and agents to call,
Our new setup will surely enthrall! 🎉


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Experiment)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@bhabalan bhabalan changed the base branch from next to feat/wxcc December 6, 2024 09:12
@bhabalan bhabalan changed the base branch from feat/wxcc to wxcc December 6, 2024 09:15
@bhabalan
Copy link
Contributor Author

bhabalan commented Dec 6, 2024

@coderabbitai review

Copy link

coderabbitai bot commented Dec 6, 2024

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

Inline review comments failed to post. This is likely due to GitHub's limits when posting large numbers of comments.

🛑 Comments failed to post (31)
packages/@webex/plugin-cc/src/services/index.ts (1)

31-35: 🛠️ Refactor suggestion

Use class name instead of 'this' in static context for clarity

In the getInstance static method, using this can be confusing because it refers to the class itself. It's clearer and more maintainable to use the class name directly.

Apply this diff to replace this with Services:

public static getInstance(options: {
  webex: WebexSDK;
  connectionConfig: SubscribeRequest;
}): Services {
-   if (!this.instance) {
-     this.instance = new Services(options);
+   if (!Services.instance) {
+     Services.instance = new Services(options);
    }

-   return this.instance;
+   return Services.instance;
  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    if (!Services.instance) {
      Services.instance = new Services(options);
    }

    return Services.instance;
🧰 Tools
🪛 Biome (1.9.4)

[error] 31-31: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 32-32: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 35-35: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)

packages/@webex/plugin-cc/src/services/WebCallingService.ts (1)

48-63: ⚠️ Potential issue

Ensure timeout is cleared on both resolve and reject

In the registerWebCallingLine method, if the registration times out, the timeout is cleared in the resolve path but not in the reject path. This could lead to memory leaks.

Apply this diff to clear the timeout in both cases:

return new Promise<void>((resolve, reject) => {
  const timeout = setTimeout(() => {
+   clearTimeout(timeout);
    reject(new Error('WebCallingService Registration timed out'));
  }, TIMEOUT_DURATION);

  this.line.on(LINE_EVENTS.REGISTERED, (deviceInfo: ILine) => {
    clearTimeout(timeout);
    LoggerProxy.log(
      `WxCC-SDK: Desktop registered successfully, mobiusDeviceId: ${deviceInfo.mobiusDeviceId}`,
      {module: WEB_CALLING_SERVICE_FILE, method: this.registerWebCallingLine.name}
    );
    resolve();
  });
  this.line.register();
});
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    return new Promise<void>((resolve, reject) => {
      const timeout = setTimeout(() => {
        clearTimeout(timeout);
        reject(new Error('WebCallingService Registration timed out'));
      }, TIMEOUT_DURATION);

      this.line.on(LINE_EVENTS.REGISTERED, (deviceInfo: ILine) => {
        clearTimeout(timeout);
        LoggerProxy.log(
          `WxCC-SDK: Desktop registered successfully, mobiusDeviceId: ${deviceInfo.mobiusDeviceId}`,
          {module: WEB_CALLING_SERVICE_FILE, method: this.registerWebCallingLine.name}
        );
        resolve();
      });
      this.line.register();
    });
  }
packages/@webex/plugin-cc/src/services/core/Err.ts (1)

63-77: 🛠️ Refactor suggestion

Pass the error message to the superclass constructor

In the Message class constructor, passing the error message to super() ensures that the Error base class properly initializes the message and stack trace. This enhances error handling and debugging.

Apply this diff to pass the message to the Error constructor:

constructor(id: IdsMessage, value?: string | Error) {
- super();
+ super(typeof value === 'string' ? value : value?.message);

  this.id = id;
- this.stack = new Error().stack!;

  if (typeof value === 'string') {
-   this.message = value;
  } else if (value instanceof Error) {
-   this.message = value.message;
    this.name = value.name;
  } else {
-   this.message = '';
  }
}

Repeat similar changes in the Details class constructor.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

  constructor(id: IdsMessage, value?: string | Error) {
    super(typeof value === 'string' ? value : value?.message);

    this.id = id;

    if (typeof value === 'string') {
    } else if (value instanceof Error) {
      this.name = value.name;
    } else {
    }
  }
packages/@webex/plugin-cc/src/services/core/WebSocket/connection-service.ts (2)

137-140: ⚠️ Potential issue

Handle potential errors in async 'setInterval' callback

The handleSocketClose method can throw an error, but within the setInterval, any rejected promises won't be caught, leading to unhandled promise rejections.

Wrap the async function in a try-catch block:

this.reconnectInterval = setInterval(async () => {
+ try {
    await this.handleSocketClose();
+ } catch (error) {
+   LoggerProxy.error(`Error during socket reconnection attempt: ${error}`, {
+     module: CONNECTION_SERVICE_FILE,
+     method: this.onSocketClose.name,
+   });
+ }
}, CONNECTIVITY_CHECK_INTERVAL);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    this.reconnectInterval = setInterval(async () => {
      try {
        await this.handleSocketClose();
      } catch (error) {
        LoggerProxy.error(`Error during socket reconnection attempt: ${error}`, {
          module: CONNECTION_SERVICE_FILE,
          method: this.onSocketClose.name,
        });
      }
    }, CONNECTIVITY_CHECK_INTERVAL);
  };

113-116: ⚠️ Potential issue

Avoid potential 'false' timeout value in 'setTimeout'

In the setTimeout call, using an expression that might evaluate to false can result in an unintended timeout value of 0. This could cause handleRestoreFailed to be called immediately.

Ensure that the timeout duration is a valid number. Apply this diff:

this.restoreTimer = setTimeout(
  this.handleRestoreFailed,
- this.connectionProp && this.connectionProp.lostConnectionRecoveryTimeout
+ this.connectionProp?.lostConnectionRecoveryTimeout ?? DEFAULT_TIMEOUT
);

Replace DEFAULT_TIMEOUT with an appropriate default value.

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 Biome (1.9.4)

[error] 115-115: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

packages/@webex/plugin-cc/src/services/core/WebSocket/WebSocketManager.ts (1)

90-100: ⚠️ Potential issue

Ensure the Promise in connect method resolves or rejects appropriately

The connect method returns a Promise that may remain pending indefinitely because it only rejects on this.websocket.onerror but never resolves on successful connection. To prevent the Promise from hanging, consider resolving it in the onopen handler.

Apply this diff to resolve the Promise upon successful WebSocket connection:

 return new Promise((resolve, reject) => {
   this.websocket.onopen = () => {
     this.isSocketClosed = false;
     this.shouldReconnect = true;

     this.websocket.send(JSON.stringify({keepalive: 'true'}));
     // ... existing code ...

+    resolve();
   };

   this.websocket.onerror = (event: any) => {
     // ... existing error handling ...

Committable suggestion skipped: line range outside the PR's diff.

docs/samples/contact-center/app.js (1)

115-116: ⚠️ Potential issue

Remove hardcoded client credentials to prevent security vulnerabilities.

Hardcoding client_id and client_secret in the source code exposes sensitive information and poses significant security risks. It's recommended to use environment variables or a secure configuration service to manage these credentials securely.

Apply this diff to remove the hardcoded credentials:

         credentials: {
-          client_id: 'C70599433db154842e919ad9e18273d835945ff198251c82204b236b157b3a213',
-          client_secret: '575ba9f5034f8a28dfef2770870c50bfc6e0b2b749f14e6a14845a1a47622f87',
+          client_id: process.env.WEBEX_CLIENT_ID,
+          client_secret: process.env.WEBEX_CLIENT_SECRET,
           redirect_uri: redirectUri,
           scope: requestedScopes,
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

        client_id: process.env.WEBEX_CLIENT_ID,
        client_secret: process.env.WEBEX_CLIENT_SECRET,
🧰 Tools
🪛 Gitleaks (8.21.2)

116-116: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

packages/@webex/plugin-cc/test/unit/spec/services/core/aqm-reqs.ts (1)

68-69: 🛠️ Refactor suggestion

Avoid using Function as a type; explicitly define the function signature.

Using Function as a type is discouraged because it accepts any function-like value, which can lead to bugs. Define the function signature explicitly for better type safety.

Apply this diff to specify the function type:

-        const eventListeners: { [key: string]: Function[] } = {};
+        const eventListeners: { [key: string]: Array<(...args: any[]) => void> } = {};

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 Biome (1.9.4)

[error] 68-68: Don't use 'Function' as a type.

Prefer explicitly define the function shape. This type accepts any function-like value, which can be a common source of bugs.

(lint/complexity/noBannedTypes)


[error] 69-69: Don't use 'Function' as a type.

Prefer explicitly define the function shape. This type accepts any function-like value, which can be a common source of bugs.

(lint/complexity/noBannedTypes)

packages/@webex/plugin-cc/test/unit/spec/cc.ts (2)

585-663: ⚠️ Potential issue

Add error handling for potential null agentConfig

In the getBuddyAgents tests, webex.cc.agentConfig is accessed without checking if it's defined. Ensure that agentConfig is properly initialized before accessing its properties to prevent runtime errors.


95-102: ⚠️ Potential issue

Ensure proper handling of asynchronous code in tests

In the test should initialize services and logger proxy on READY event, the assertions are inside an event handler callback. Jest may not wait for the asynchronous code to complete before finishing the test. To ensure the test waits for the event, consider using the done callback or returning a Promise.

Apply this diff to fix the test using the done callback:

-it('should initialize services and logger proxy on READY event', () => {
+it('should initialize services and logger proxy on READY event', (done) => {
   webex.once('READY', () => {
     expect(Services.getInstance).toHaveBeenCalled();
     expect(LoggerProxy.initialize).toHaveBeenCalledWith(webex.logger);
+    done();
   });
   webex.emit('READY');
 });

Alternatively, you can return a Promise:

-it('should initialize services and logger proxy on READY event', () => {
+it('should initialize services and logger proxy on READY event', async () => {
+  await new Promise((resolve) => {
     webex.once('READY', () => {
       expect(Services.getInstance).toHaveBeenCalled();
       expect(LoggerProxy.initialize).toHaveBeenCalledWith(webex.logger);
+      resolve();
     });
+  });
   webex.emit('READY');
 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

  it('should initialize services and logger proxy on READY event', (done) => {
    webex.once('READY', () => {
      expect(Services.getInstance).toHaveBeenCalled();
      expect(LoggerProxy.initialize).toHaveBeenCalledWith(webex.logger);
      done();
    });

    webex.emit('READY');
  });
  it('should initialize services and logger proxy on READY event', async () => {
    await new Promise((resolve) => {
      webex.once('READY', () => {
        expect(Services.getInstance).toHaveBeenCalled();
        expect(LoggerProxy.initialize).toHaveBeenCalledWith(webex.logger);
        resolve();
      });
    });

    webex.emit('READY');
  });
packages/@webex/plugin-cc/src/config.ts (1)

20-21: ⚠️ Potential issue

Replace hardcoded domain with environment-based configuration

The TODO comment indicates this domain should be dynamic, but it's currently hardcoded to a production URL. This could cause issues when switching between environments.

Consider implementing environment-based configuration:

- domain: 'rtw.prod-us1.rtmsprod.net',
+ domain: process.env.CC_DOMAIN || 'rtw.prod-us1.rtmsprod.net',
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

        // TODO: This should be dynamic based on the environment
        domain: process.env.CC_DOMAIN || 'rtw.prod-us1.rtmsprod.net',
packages/@webex/plugin-cc/src/services/core/HttpRequest.ts (1)

20-34: 🛠️ Refactor suggestion

Add error handling and request validation

The request method should include:

  1. Request timeout
  2. Error handling with proper types
  3. Input validation for required fields
  public async request(options: {
    service: string;
    resource: string;
    method: HTTP_METHODS;
    body?: RequestBody;
  }): Promise<IHttpResponse> {
    const {service, resource, method, body} = options;

+   if (!service || !resource || !method) {
+     throw new Error('Missing required request parameters');
+   }

    return this.webex.request({
      service,
      resource,
      method,
      body,
+     timeout: 30000, // 30 seconds timeout
    }).catch((error) => {
+     throw createErrDetailsObject(error);
    });
  }

Committable suggestion skipped: line range outside the PR's diff.

packages/@webex/plugin-cc/src/services/core/Utils.ts (1)

13-25: 🛠️ Refactor suggestion

Enhance error details logging and typing

The error handling could be improved with better typing and structured logging:

-export const getErrorDetails = (error: any, methodName: string, moduleName: string) => {
+export const getErrorDetails = (
+  error: Error & { details?: Failure },
+  methodName: string,
+  moduleName: string
+) => {
   const failure = error.details as Failure;
   const reason = failure?.data?.reason ?? `Error while performing ${methodName}`;
+  
+  const errorContext = {
+    module: moduleName,
+    method: methodName,
+    trackingId: failure?.trackingId,
+    error: error.message,
+  };
+
   LoggerProxy.error(`${methodName} failed with trackingId: ${failure?.trackingId}`, {
-    module: moduleName,
-    method: methodName,
+    ...errorContext,
   });

   return {
     error: new Error(reason ?? `Error while performing ${methodName}`),
     reason,
+    context: errorContext,
   };
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

export const getErrorDetails = (
  error: Error & { details?: Failure },
  methodName: string,
  moduleName: string
) => {
  const failure = error.details as Failure;
  const reason = failure?.data?.reason ?? `Error while performing ${methodName}`;
  
  const errorContext = {
    module: moduleName,
    method: methodName,
    trackingId: failure?.trackingId,
    error: error.message,
  };

  LoggerProxy.error(`${methodName} failed with trackingId: ${failure?.trackingId}`, {
    ...errorContext,
  });

  return {
    error: new Error(reason ?? `Error while performing ${methodName}`),
    reason,
    context: errorContext,
  };
};
packages/@webex/plugin-cc/jest.config.js (1)

5-5: 🛠️ Refactor suggestion

Consider specifying transformIgnorePatterns

An empty transformIgnorePatterns array means Jest will transform all files, including those in node_modules. This could significantly slow down your tests.

-  transformIgnorePatterns: [],
+  transformIgnorePatterns: ['/node_modules/(?!(@webex)/)'],
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

  transformIgnorePatterns: ['/node_modules/(?!(@webex)/)'],
packages/@webex/plugin-cc/src/services/core/types.ts (2)

14-14: 🛠️ Refactor suggestion

Avoid using 'any' type

The data property in the Bind interface uses any. Consider using a generic type parameter for better type safety.

-interface Bind {
+interface Bind<T = unknown> {
   type: BindType;
-  data?: any;
+  data?: T;
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

interface Bind<T = unknown> {
  type: BindType;
  data?: T;
}

48-48: 🛠️ Refactor suggestion

Replace void in union type

As flagged by static analysis, using void in a union type can be confusing. Consider using undefined instead.

-export type CbRes<TRes> = (res: any) => void | TRes;
+export type CbRes<TRes> = (res: unknown) => undefined | TRes;

Also, replace any with unknown for better type safety.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

export type CbRes<TRes> = (res: unknown) => undefined | TRes;
🧰 Tools
🪛 Biome (1.9.4)

[error] 48-48: void is confusing inside a union type.

Unsafe fix: Use undefined instead.

(lint/suspicious/noConfusingVoidType)

packages/@webex/plugin-cc/README.md (1)

16-18: ⚠️ Potential issue

Add documentation links

The TODO comment indicates missing documentation links. These should be completed before merging.

-TODO: Add the documentation links once ready
-- [Introduction to the Webex Web Calling SDK]()
-- [Quickstart guide]().
+- [Introduction to the Webex Web Calling SDK](link-to-introduction)
+- [Quickstart guide](link-to-quickstart)

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 Markdownlint (0.35.0)

17-17: null
No empty links

(MD042, no-empty-links)


18-18: null
No empty links

(MD042, no-empty-links)

packages/@webex/plugin-cc/src/logger-proxy.ts (1)

4-48: 🛠️ Refactor suggestion

Consider refactoring to a module with standalone functions

The current implementation uses a static-only class which goes against TypeScript best practices. Consider refactoring to a module with standalone functions for better maintainability and testing.

-export default class LoggerProxy {
-  public static logger: Logger;
+let logger: Logger;
 
-  public static initialize(logger: Logger): void {
-    LoggerProxy.logger = logger;
+export function initialize(loggerInstance: Logger): void {
+  logger = loggerInstance;
   }

-  public static log(message: string, context: LogContext = {}): void {
-    if (LoggerProxy.logger) {
-      LoggerProxy.logger.log(LoggerProxy.format(LOGGING_LEVEL.log, message, context));
+export function log(message: string, context: LogContext = {}): void {
+  if (logger) {
+    logger.log(format(LOGGING_LEVEL.log, message, context));
     }
   }
   // ... similar changes for other methods

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 Biome (1.9.4)

[error] 4-48: Avoid classes that contain only static members.

Prefer using simple functions instead of classes with only static members.

(lint/complexity/noStaticOnlyClass)

packages/@webex/plugin-cc/src/services/config/constants.ts (1)

16-46: ⚠️ Potential issue

Add URL parameter encoding for security

The endpoint mapping functions don't encode URL parameters, which could lead to URL injection vulnerabilities.

 listTeams: (
   orgId: string,
   page: number,
   pageSize: number,
   filter: string[],
   attributes: string[]
 ) =>
-  `organization/${orgId}/v2/team?page=${page}&pageSize=${pageSize}${
-    filter && filter.length > 0 ? `&filter=id=in=${filter}` : ''
-  }&attributes=${attributes}`,
+  `organization/${encodeURIComponent(orgId)}/v2/team?page=${encodeURIComponent(page)}&pageSize=${encodeURIComponent(pageSize)}${
+    filter && filter.length > 0 ? `&filter=id=in=${encodeURIComponent(filter.join(','))}` : ''
+  }&attributes=${encodeURIComponent(attributes.join(','))}`,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

export const endPointMap = {
  userByCI: (orgId: string, agentId: string) =>
    `organization/${orgId}/user/by-ci-user-id/${agentId}`,
  desktopProfile: (orgId: string, desktopProfileId: string) =>
    `organization/${orgId}/agent-profile/${desktopProfileId}`,
  listTeams: (
    orgId: string,
    page: number,
    pageSize: number,
    filter: string[],
    attributes: string[]
  ) =>
    `organization/${encodeURIComponent(orgId)}/v2/team?page=${encodeURIComponent(page)}&pageSize=${encodeURIComponent(pageSize)}${
      filter && filter.length > 0 ? `&filter=id=in=${encodeURIComponent(filter.join(','))}` : ''
    }&attributes=${encodeURIComponent(attributes.join(','))}`,
  listAuxCodes: (
    orgId: string,
    page: number,
    pageSize: number,
    filter: string[],
    attributes: string[]
  ) =>
    `organization/${orgId}/v2/auxiliary-code?page=${page}&pageSize=${pageSize}${
      filter && filter.length > 0 ? `&filter=id=in=${filter}` : ''
    }&attributes=${attributes}`,
  orgInfo: (orgId: string) => `organization/${orgId}`,
  orgSettings: (orgId: string) => `organization/${orgId}/v2/organization-setting?agentView=true`,
  tenantData: (orgId: string) => `organization/${orgId}/v2/tenant-configuration?agentView=true`,
  urlMapping: (orgId: string) => `organization/${orgId}/v2/org-url-mapping?sort=name,ASC`,
  dialPlan: (orgId: string) => `organization/${orgId}/dial-plan?agentView=true`,
};
packages/@webex/plugin-cc/test/unit/spec/services/agent/index.ts (1)

43-43: 🛠️ Refactor suggestion

Avoid using 'any' type in TypeScript tests

The test data uses any type which bypasses TypeScript's type checking benefits. Consider creating proper interface types for the test data.

- const req = await agent.stationLogin({data: {} as any});
+ interface StationLoginData {
+   // Add required properties based on actual implementation
+   stationId?: string;
+   extension?: string;
+ }
+ const req = await agent.stationLogin({data: {} as StationLoginData});

Also applies to: 50-50, 56-56

packages/webex/src/webex.js (1)

30-30: 💡 Codebase verification

Warning: Duplicate plugin registration detected

The CC plugin is being required in both webex.js and contact-center.js, which could lead to initialization issues. The plugin should only be registered once.

  • Remove the duplicate require('@webex/plugin-cc') from either webex.js or contact-center.js
  • Recommend keeping it in webex.js where other plugins are registered for consistency
🔗 Analysis chain

LGTM: CC plugin integration follows established patterns

The Contact Center plugin is properly integrated following the existing plugin registration pattern.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Verify CC plugin is properly exported and documented
rg -g '!node_modules' -g '*.{js,ts,md}' '@webex/plugin-cc'

Length of output: 478


Script:

#!/bin/bash
# Check if plugin is properly registered and exported
ast-grep --pattern 'export default $_ = $_;'  packages/@webex/plugin-cc/src/*.js

# Check if plugin is properly documented
cat packages/@webex/plugin-cc/README.md

# Check for duplicate requires
rg -l "require\('@webex/plugin-cc'\)" packages/webex/src/

Length of output: 1844


Script:

#!/bin/bash
# Check content of contact-center.js to understand duplicate require
cat packages/webex/src/contact-center.js

# Check if plugin is properly registered in index files
rg -g '*.js' -l "ContactCenter" packages/@webex/plugin-cc/

Length of output: 550

packages/@webex/plugin-cc/src/services/core/WebSocket/keepalive.worker.js (2)

31-38: ⚠️ Potential issue

Consider making timeout duration configurable

The comment mentions a 16s timeout, but the actual timeout uses closeSocketTimeout which defaults to 5000ms (5s). This discrepancy between the comment and implementation could lead to confusion.

-    // Sets a timeout of 16s, checks if socket didn't close then it closes forcefully
+    // Sets a timeout using closeSocketTimeout (default: 5s), checks if socket didn't close then it closes forcefully
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    // Sets a timeout using closeSocketTimeout (default: 5s), checks if socket didn't close then it closes forcefully
    timeOutId = setTimeout(() => {
      if (!isSocketClosed) {
        initiateWebSocketClosure = true;
        postMessage({ type: "closeSocket" });
      }
    }, closeSocketTimeout);
  }

51-74: 🛠️ Refactor suggestion

Enhance error handling in message event listener

The message event listener should include error handling for malformed messages and invalid interval durations.

 addEventListener("message", (event) => {
+  try {
     if (event.data?.type === "start") {
+      if (intervalDuration < 1000) {
+        throw new Error("Invalid interval duration: must be at least 1000ms");
+      }
       intervalDuration = event.data?.intervalDuration || 4000;
       closeSocketTimeout = event.data?.closeSocketTimeout || 5000;
       console.log("event=Websocket startWorker | keepalive Worker started");
       intervalId = setInterval(
         (checkIfSocketClosed) => {
           checkNetworkStatus();
           isSocketClosed = checkIfSocketClosed;
         },
         intervalDuration,
         event.data?.isSocketClosed
       );

       resetOfflineHandler();
     }

     if (event.data?.type === "terminate" && intervalId) {
       console.log("event=Websocket terminateWorker | keepalive Worker stopped");
       clearInterval(intervalId);
       intervalId = null;
       resetOfflineHandler();
     }
+  } catch (error) {
+    console.error("Error in keepalive worker:", error);
+    postMessage({ type: "error", error: error.message });
+  }
 });

Committable suggestion skipped: line range outside the PR's diff.

packages/@webex/plugin-cc/src/services/agent/types.ts (2)

50-51: 🛠️ Refactor suggestion

Consolidate agent status/subStatus types

The status and subStatus types are inconsistently defined across different message types. Consider creating shared enums or type unions for these values.

+export type AgentStatus = 'LoggedIn' | 'LoggedOut' | string;
+export type AgentSubStatus = 'Available' | 'Idle' | string;

 export type StateChangeSuccess = Msg<{
   // ...
-  status: string;
-  subStatus: 'Available' | 'Idle';
+  status: AgentStatus;
+  subStatus: AgentSubStatus;
   // ...
 }>;

 export type StationLoginSuccess = Msg<{
   // ...
-  status: string;
-  subStatus: 'Available' | 'Idle';
+  status: AgentStatus;
+  subStatus: AgentSubStatus;
   // ...
 }>;

Also applies to: 70-71


85-85: 🛠️ Refactor suggestion

Define exhaustive agent states

The AgentState type uses a string fallback which could lead to type safety issues. Consider defining all possible states explicitly.

-export type AgentState = 'Available' | 'Idle' | 'RONA' | string;
+export type AgentState =
+  | 'Available'
+  | 'Idle'
+  | 'RONA'
+  | 'Busy'
+  | 'OnBreak'
+  | 'Training'
+  | 'Meeting'
+  | 'Reserved';

Committable suggestion skipped: line range outside the PR's diff.

packages/@webex/plugin-cc/test/unit/spec/services/WebCallingService.ts (1)

103-145: 🛠️ Refactor suggestion

Enhance incoming call test coverage

The incoming call test could be more comprehensive:

  1. Missing test for call rejection scenarios
  2. No verification of call properties
  3. Event listener cleanup not handled

Add the following test cases:

it('should handle call rejection', async () => {
  // Setup
  line = callingClient.getLines().line1 as ILine;
  const callObj = {
    on: jest.fn(),
    reject: jest.fn(),
  } as unknown as ICall;
  
  // Test
  await webRTCCalling.registerWebCallingLine();
  await webRTCCalling.rejectCall(callObj);
  
  // Assert
  expect(callObj.reject).toHaveBeenCalled();
});

afterEach(() => {
  // Cleanup event listeners
  window.removeEventListener('line:incoming_call', eventListener);
});
docs/samples/contact-center/index.html (3)

14-34: 🛠️ Refactor suggestion

Improve accessibility for expand/collapse buttons

The expand/collapse functionality needs accessibility improvements.

-    <button onclick="expandAll()" class="global-toggle-btn" title="Expand all">
+    <button 
+      onclick="expandAll()" 
+      class="global-toggle-btn" 
+      title="Expand all"
+      aria-expanded="false"
+      aria-controls="all-sections">

Also applies to: 37-42


61-62: ⚠️ Potential issue

Enhance security for access token input

The access token input should be treated as sensitive information.

-              <input id="access-token" name="accessToken" placeholder="Access Token" value="" type="text">
+              <input id="access-token" name="accessToken" placeholder="Access Token" value="" type="password" autocomplete="off">
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

              <input id="access-token" name="accessToken" placeholder="Access Token" value="" type="password" autocomplete="off">
              <button id="access-token-save" class="btn-code" type="submit">webex.init()</button>

145-147: 💡 Codebase verification

Missing required dependency: contact-center.min.js

The script loading sequence in the HTML file has a critical issue:

  • webex.min.js exists and is correctly referenced
  • contact-center.min.js is missing from the expected path
  • app.js exists and depends on both scripts, as evidenced by the initialization code

The missing contact-center.min.js will cause runtime errors as app.js tries to access webex.cc.register().

🔗 Analysis chain

Verify script dependencies and loading order

The script loading order is critical for proper initialization.

Let's check the dependencies:


Let me check the content of app.js to understand the initialization sequence and dependencies.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check if the required scripts exist
ls docs/samples/webex.min.js docs/samples/contact-center.min.js docs/samples/contact-center/app.js 2>/dev/null || echo "Missing required scripts"

# Verify script contents
rg -l "initWebex|initContactCenter" docs/samples/contact-center/

Length of output: 322


Script:

#!/bin/bash
# Check the content of app.js to understand initialization
rg -A 5 "initWebex|initContactCenter" docs/samples/contact-center/app.js

# Check if contact-center.min.js is in a different location
fd -g "*contact-center.min.js" 

Length of output: 697

webpack.config.js (1)

50-56: 💡 Codebase verification

Entry point file exists but needs CommonJS to UMD export alignment

The contact-center entry point exists but uses module.exports (CommonJS) while the webpack config expects a UMD module with a default export. This mismatch should be addressed:

  • packages/webex/src/contact-center.js: Change module.exports = Webex to export default Webex to align with UMD library configuration
🔗 Analysis chain

Verify contact-center entry point configuration

The configuration looks correct but needs verification of the entry point file.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Verify the entry point file exists
test -f packages/webex/src/contact-center.js || echo "Entry point file missing"

# Check exports in the entry file
ast-grep --pattern 'export default $_' packages/webex/src/contact-center.js

Length of output: 124


Script:

#!/bin/bash
# Let's first check if the file exists in a different location or with a different extension
fd "contact-center.js" --type f

# Let's also check for any references to contact-center in the codebase
rg -l "contact-center" --type js

Length of output: 119


Script:

#!/bin/bash
# Let's examine the content of the contact-center.js file
cat packages/webex/src/contact-center.js

# Also check if there are any imports to this file
rg -l "import.*contact-center" --type js

Length of output: 552

packages/@webex/internal-plugin-mercury/test/unit/spec/socket.js (1)

203-243: ⚠️ Potential issue

Fix syntax error in test cases for authorizationRequired option.

The test cases have syntax errors:

  1. Missing promise chain in both test cases
  2. Incorrect indentation

Apply this diff to fix the syntax:

  it('accepts authorizationRequired option as false and skip authorize', () => {
    const s = new Socket();
    const authorizeSpy = sinon.spy(socket, '_authorize');
-   socket.open('ws://example.com', {
+   return socket.open('ws://example.com', {
      forceCloseDelay: mockoptions.forceCloseDelay,
      pingInterval: mockoptions.pingInterval,
      pongTimeout: mockoptions.pongTimeout,
      logger: console,
      token: 'mocktoken',
      trackingId: 'mocktrackingid',
      logLevelToken: 'mocklogleveltoken',
      authorizationRequired: false,
    }).then(() => {
      mockWebSocket.readyState = 1;
      mockWebSocket.emit('open');

      assert.notCalled(authorizeSpy);
      assert.called(socket._ping);
    });
  });

  it('accepts authorizationRequired option as true and calles authorize', () => {
    const s = new Socket();
    const authorizeSpy = sinon.spy(socket, '_authorize');
-   socket.open('ws://example.com', {
+   return socket.open('ws://example.com', {
      forceCloseDelay: mockoptions.forceCloseDelay,
      pingInterval: mockoptions.pingInterval,
      pongTimeout: mockoptions.pongTimeout,
      logger: console,
      token: 'mocktoken',
      trackingId: 'mocktrackingid',
      logLevelToken: 'mocklogleveltoken',
      authorizationRequired: true,
    }).then(() => {
      mockWebSocket.readyState = 1;
      mockWebSocket.emit('open');

      assert.called(authorizeSpy);
      assert.called(socket._ping);
    });
  });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

      it('accepts authorizationRequired option as false and skip authorize', () => {
        const s = new Socket();
        const authorizeSpy = sinon.spy(socket, '_authorize');
        return socket.open('ws://example.com', {
          forceCloseDelay: mockoptions.forceCloseDelay,
          pingInterval: mockoptions.pingInterval,
          pongTimeout: mockoptions.pongTimeout,
          logger: console,
          token: 'mocktoken',
          trackingId: 'mocktrackingid',
          logLevelToken: 'mocklogleveltoken',
          authorizationRequired: false,
        }).then(() => {
          mockWebSocket.readyState = 1;
          mockWebSocket.emit('open');

          assert.notCalled(authorizeSpy);
          assert.called(socket._ping);
        });
      });

      it('accepts authorizationRequired option as true and calles authorize', () => {
        const s = new Socket();
        const authorizeSpy = sinon.spy(socket, '_authorize');
        return socket.open('ws://example.com', {
          forceCloseDelay: mockoptions.forceCloseDelay,
          pingInterval: mockoptions.pingInterval,
          pongTimeout: mockoptions.pongTimeout,
          logger: console,
          token: 'mocktoken',
          trackingId: 'mocktrackingid',
          logLevelToken: 'mocklogleveltoken',
          authorizationRequired: true,
        }).then(() => {
          mockWebSocket.readyState = 1;
          mockWebSocket.emit('open');

          assert.called(authorizeSpy);
          assert.called(socket._ping);
        });
      });

Copy link

This pull request is automatically being deployed by Amplify Hosting (learn more).

Access this pull request here: https://pr-4022.d3m3l2kee0btzx.amplifyapp.com

Copy link
Contributor

@sreenara sreenara left a comment

Choose a reason for hiding this comment

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

Please list the manual test scenarios in the PR description as well more details about the integration and its current scopes.

@bhabalan
Copy link
Contributor Author

bhabalan commented Dec 6, 2024

@CodeRabbit review

Copy link

coderabbitai bot commented Dec 6, 2024

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Outside diff range and nitpick comments (2)
docs/samples/contact-center/app.js (2)

88-111: Consider consolidating scope declarations

While the scopes are well documented, the current implementation could be improved by:

  1. Moving scopes to a configuration object
  2. Combining related scopes into named groups
  3. Using a more efficient method to combine arrays
-  const ccMandatoryScopes = [
-    "cjp:config_read",
-    "cjp:config_write",
-    "cjp:config",
-    "cjp:user",
-  ];
-
-  const webRTCCallingScopes = [
-    "spark:webrtc_calling",
-    "spark:calls_read",
-    "spark:calls_write",
-    "spark:xsi"
-  ];
-
-  const additionalScopes = [
-    "spark:kms",
-  ];
-
-  const requestedScopes = Array.from(
-    new Set(
-        ccMandatoryScopes
-        .concat(webRTCCallingScopes)
-        .concat(additionalScopes))
-      ).join(' ');
+  const OAUTH_SCOPES = {
+    contactCenter: [
+      "cjp:config_read",
+      "cjp:config_write",
+      "cjp:config",
+      "cjp:user",
+    ],
+    webRTCCalling: [
+      "spark:webrtc_calling",
+      "spark:calls_read",
+      "spark:calls_write",
+      "spark:xsi"
+    ],
+    additional: ["spark:kms"]
+  };
+
+  const requestedScopes = [...new Set([
+    ...OAUTH_SCOPES.contactCenter,
+    ...OAUTH_SCOPES.webRTCCalling,
+    ...OAUTH_SCOPES.additional
+  ])].join(' ');

124-124: Consider using sessionStorage for OAuth state

Using localStorage for OAuth state persistence could lead to state inconsistencies across tabs. Consider using sessionStorage instead, which is already used for access tokens in this application.

- localStorage.setItem('OAuth', true);
+ sessionStorage.setItem('OAuth', true);
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 3372479 and 02f5aeb.

📒 Files selected for processing (1)
  • docs/samples/contact-center/app.js (5 hunks)
🧰 Additional context used
🪛 Gitleaks (8.21.2)
docs/samples/contact-center/app.js

117-117: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🔇 Additional comments (3)
docs/samples/contact-center/app.js (3)

27-28: LGTM: DOM element references properly declared

The new OAuth-related DOM element references follow the existing patterns and best practices.


59-63: LGTM: OAuth type handling properly implemented

The OAuth case is properly integrated into the existing authentication type switch statement, maintaining consistency with the existing pattern.


222-224: LGTM: Proper handling of idle codes list

The condition properly handles the agent status button enablement based on the idle codes list availability.

docs/samples/contact-center/app.js Outdated Show resolved Hide resolved
docs/samples/contact-center/app.js Outdated Show resolved Hide resolved
Copy link
Contributor

@mkesavan13 mkesavan13 left a comment

Choose a reason for hiding this comment

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

Just a couple of nitpicks. Please ensure to have addressed/responded to all the code rabbit reviews. Approving

docs/samples/contact-center/app.js Outdated Show resolved Hide resolved
docs/samples/contact-center/app.js Outdated Show resolved Hide resolved
Copy link

gitguardian bot commented Dec 12, 2024

⚠️ GitGuardian has uncovered 1 secret following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

Since your pull request originates from a forked repository, GitGuardian is not able to associate the secrets uncovered with secret incidents on your GitGuardian dashboard.
Skipping this check run and merging your pull request will create secret incidents on your GitGuardian dashboard.

🔎 Detected hardcoded secret in your pull request
GitGuardian id GitGuardian status Secret Commit Filename
- - Generic High Entropy Secret 19a528e docs/samples/contact-center/app.js View secret
🛠 Guidelines to remediate hardcoded secrets
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secret safely. Learn here the best practices.
  3. Revoke and rotate this secret.
  4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

@bhabalan bhabalan enabled auto-merge (squash) December 16, 2024 06:59
@bhabalan bhabalan merged commit 6fa4910 into webex:wxcc Dec 16, 2024
11 of 12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
validated If the pull request is validated for automation.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants