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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 155 additions & 0 deletions extensions/cxone/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
# CXone Extension

This Cognigy extension integrates with **CXone**, providing authenticated HTTP request capabilities for CXone API integration in your Cognigy flows.

## Installation

1. Build the extension:
```bash
npm install
npm run build
```

2. Upload the generated `cxone-*.tar.gz` file to your Cognigy.AI instance via **Manage > Extensions > Upload Extension**

## CXone Authenticated Call Node

Makes authenticated HTTP requests to any API using CXone bearer tokens.

### Features
- Automatic token injection from `input.data.cxonetoken` or `context.cxonetoken`
- Multiple HTTP methods (GET, POST, PUT, PATCH, DELETE)
- JSON, Text, and Form data payload types
- Configurable timeout (1-20 seconds, default 8s)
- Automatic retry with exponential backoff
- Request/response logging with sensitive data redaction
- Response header storage option
- Structured error handling
- Configurable response storage (input or context, custom key)
- Optional insecure SSL support for development environments

### Configuration

#### Basic
- **URL** (`url`): Complete endpoint URL.
- **HTTP Method** (`method`): One of `GET`, `POST`, `PUT`, `PATCH`, `DELETE`.

#### Headers
- **Headers** (`headers`): Additional request headers as JSON object (e.g. `{"X-Custom": "value"}`).
The `Authorization` header is injected automatically from the CXone token.
- **Store Response Headers** (`storeResponseHeaders`): When enabled, response headers are stored together with the body in the configured target.

#### Payload
- **Payload Type** (`payloadType`): Selects how to send the request body for non-DELETE methods (including GET with body for complex queries):
- `json`: uses **Request Body (JSON)**.
- `text`: uses **Request Body (Text)**.
- `form`: uses **Request Body (Form Data)**.

**Note**: While GET requests with body data are non-standard HTTP practice, some APIs require complex query parameters that are better suited to request bodies.
- **Request Body (JSON)** (`bodyJson`): JSON object body (e.g. `{"foo": "bar"}`).
- **Request Body (Text)** (`bodyText`): Raw text body.
- **Request Body (Form Data)** (`bodyForm`): JSON object treated as key/value pairs for form data.

#### Execution
- **Timeout (ms)** (`timeoutMs`): Request timeout in milliseconds (1,000–20,000). There is still a hard 20s execution budget in Cognigy.
- **Enable Retry** (`enableRetry`): When enabled, the node automatically retries on network errors, timeouts and server errors.
- **Retry Attempts** (`retryAttempts`): Number of additional retries (1–5). `2` means `1 initial + 2 retries = 3` total attempts. Uses exponential backoff with jitter.

#### Error handling & debug
- **Fail on Non-2xx Status** (`failOnNon2xx`):
- `true` (default): non-2xx responses are treated as errors and surfaced via structured error objects.
- `false`: non-2xx responses are treated as successful and returned with status and body.
- **Debug Mode** (`debugMode`): Enables detailed logging of request/response metadata. Sensitive values such as tokens and credentials are redacted.

#### Security
- **Allow Insecure SSL** (`allowInsecureSSL`):
Allows calls to HTTPS endpoints with self-signed or otherwise untrusted certificates.
**Use only in development/testing**, never in production.

#### Storage
- **Store Result In** (`responseTarget`): Where to store the response (`context` or `input`). Default is `context`.
- **Key to store Result** (`responseKey`): Path/key used under the selected target (default: `cxoneApiResponse`).

Example: with default storage, a successful call will store the result in `context.cxoneApiResponse`.

### Example usage

```json
{
"url": "https://api.cxone.example.com/v1/customers",
"method": "GET",
"headers": {
"X-Tenant": "my-tenant"
},
"timeoutMs": 8000,
"enableRetry": true,
"retryAttempts": 2,
"responseTarget": "context",
"responseKey": "cxoneApiResponse",
"failOnNon2xx": true,
"debugMode": false
}
```

In the flow, you can then read the response from `context.cxoneApiResponse`.

## Testing

This extension uses **Jest** for unit tests.

- **Run all tests**: `npm test`
- **Run tests in watch mode**: `npm run test:watch`

The test suite covers:
- Node descriptors in `src/nodes` (`authenticated-call.ts`) including success and error paths
- Helpers in `src/helpers` (`errors.ts`) to verify error handling and response formatting
- Jest is configured with coverage thresholds targeting near-100% coverage for helper modules

## Troubleshooting

### Common Issues

**Error: "Missing cxonetoken"**
- Ensure the flow is invoked by CXone with authentication token in `input.data.cxonetoken` or `context.cxonetoken`

**HTTP Error responses**
- Check the endpoint URL is correct and accessible
- Verify the HTTP method matches the API requirements
- Ensure request headers are properly formatted JSON

**Timeout errors**
- Increase the timeout value in node configuration (max 20 seconds)
- Check network connectivity to the target API

**Retry exhaustion**
- Check server availability and response times
- Verify API endpoint is not rate-limiting requests

## Development

### Building

```bash
npm install
npm run transpile
npm run lint
npm run build # Includes transpile, lint, and zip
```

### Project Structure

```
src/
├── nodes/ # Node implementations (authenticated-call)
├── helpers/ # Utility functions (errors)
├── test-utils/ # Test utilities and mocks
└── types/ # TypeScript type definitions
```

## License

NiCE

## Author

NiCE
Binary file added extensions/cxone/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 22 additions & 0 deletions extensions/cxone/jest.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/__tests__/**/*.spec.ts'],
moduleFileExtensions: ['ts', 'js', 'json'],
roots: ['<rootDir>/src'],
setupFilesAfterEnv: ['<rootDir>/src/test-utils/jest-setup.ts'],
collectCoverageFrom: [
'src/**/*.{ts,js}',
'!src/**/__tests__/**/*'
],
coverageThreshold: {
global: {
branches: 80,
functions: 90,
lines: 90,
statements: 90
}
}
};


32 changes: 32 additions & 0 deletions extensions/cxone/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "cxone",
"version": "1.0.28",
"description": "CXone Extension for authenticated HTTP requests to CXone APIs",
"main": "build/module.js",
"scripts": {
"transpile": "tsc -p .",
"zip": "node zip.js",
"build": "npm run transpile && npm run lint && npm run zip",
"lint": "tslint -c tslint.json src/**/*.ts",
"test": "jest",
"test:watch": "jest --watch"
},
"keywords": [
"CXone",
"HTTP Client",
"Authenticated Requests",
"CXone API"
],
"author": "NiCE",
"license": "NiCE",
"dependencies": {
"@cognigy/extension-tools": "^0.14.0"
},
"devDependencies": {
"@types/jest": "^29.5.12",
"jest": "^29.7.0",
"ts-jest": "^29.2.5",
"tslint": "^6.1.3",
"typescript": "^5.9.2"
}
}
137 changes: 137 additions & 0 deletions extensions/cxone/src/helpers/__tests__/errors.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/// <reference types="jest" />

import { CXoneError, createErrorMessage } from "../errors";

describe("errors", () => {
describe("CXoneError", () => {
it("should set all properties correctly when instantiated", () => {
const error = new CXoneError("Component", "Action", "Message", { key: "value" });

expect(error.component).toBe("Component");
expect(error.action).toBe("Action");
expect(error.message).toBe("Component: Action: Message");
expect(error.name).toBe("CXoneError");
expect(error.context).toEqual({ key: "value" });
});

it("should format error message correctly", () => {
const error = new CXoneError("TokenManager", "encryptToken", "Failed to encrypt");

expect(error.message).toBe("TokenManager: encryptToken: Failed to encrypt");
});

it("should set name property to 'CXoneError'", () => {
const error = new CXoneError("Component", "Action", "Message");

expect(error.name).toBe("CXoneError");
});

it("should work without optional context parameter", () => {
const error = new CXoneError("Component", "Action", "Message");

expect(error.component).toBe("Component");
expect(error.action).toBe("Action");
expect(error.message).toBe("Component: Action: Message");
expect(error.context).toBeUndefined();
});

it("should be an instance of Error", () => {
const error = new CXoneError("Component", "Action", "Message");

expect(error).toBeInstanceOf(Error);
});

it("should be an instance of CXoneError", () => {
const error = new CXoneError("Component", "Action", "Message");

expect(error).toBeInstanceOf(CXoneError);
});

it("should preserve stack trace when Error.captureStackTrace is available", () => {
const error = new CXoneError("Component", "Action", "Message");

// Stack trace should exist (at least in Node.js environment)
expect(error.stack).toBeDefined();
expect(typeof error.stack).toBe("string");
expect(error.stack!.length).toBeGreaterThan(0);
});

it("should handle empty strings in component, action, and message", () => {
const error = new CXoneError("", "", "");

expect(error.component).toBe("");
expect(error.action).toBe("");
expect(error.message).toBe(": : ");
});

it("should handle special characters in message", () => {
const error = new CXoneError("Component", "Action", "Error: with: colons");

expect(error.message).toBe("Component: Action: Error: with: colons");
});

it("should handle context with various data types", () => {
const context = {
string: "value",
number: 123,
boolean: true,
null: null,
array: [1, 2, 3],
nested: { key: "value" }
};
const error = new CXoneError("Component", "Action", "Message", context);

expect(error.context).toEqual(context);
});
});

describe("createErrorMessage", () => {
it("should format message correctly with component, action, and details", () => {
const message = createErrorMessage("Component", "Action", "Details");

expect(message).toBe("Component: Action: Details");
});

it("should handle various component/action/details combinations", () => {
expect(createErrorMessage("TokenManager", "encryptToken", "Failed to encrypt")).toBe(
"TokenManager: encryptToken: Failed to encrypt"
);
expect(createErrorMessage("ApiClient", "getToken", "Network error")).toBe(
"ApiClient: getToken: Network error"
);
expect(createErrorMessage("Handler", "process", "Invalid input")).toBe(
"Handler: process: Invalid input"
);
});

it("should handle empty strings", () => {
expect(createErrorMessage("", "", "")).toBe(": : ");
expect(createErrorMessage("Component", "", "Details")).toBe("Component: : Details");
expect(createErrorMessage("", "Action", "Details")).toBe(": Action: Details");
});

it("should handle special characters", () => {
expect(createErrorMessage("Comp:onent", "Act:ion", "Det:ails")).toBe(
"Comp:onent: Act:ion: Det:ails"
);
expect(createErrorMessage("Component", "Action", "Error: with: colons")).toBe(
"Component: Action: Error: with: colons"
);
});

it("should handle numbers as strings", () => {
expect(createErrorMessage("Component1", "Action2", "Details3")).toBe(
"Component1: Action2: Details3"
);
});

it("should handle long strings", () => {
const longString = "a".repeat(1000);
const message = createErrorMessage("Component", "Action", longString);

expect(message).toBe(`Component: Action: ${longString}`);
expect(message.length).toBe(1019); // Component: Action: + 1000 chars (19 + 1000)
});
});
});

Loading