├── .eslintignore ├── .eslintrc.cjs ├── .gitignore ├── .husky └── pre-commit ├── .lintstagedrc ├── .prettierignore ├── .prettierrc ├── CHANGELOG.md ├── LICENSE ├── NOTICE ├── README.md ├── app ├── README.md ├── babel.config.js ├── package.json ├── src │ ├── amazon-connect-app-config.ts │ ├── amazon-connect-app.test.ts │ ├── amazon-connect-app.ts │ ├── app-context.ts │ ├── contact-scope.ts │ ├── index.ts │ ├── lifecycle │ │ ├── index.ts │ │ ├── lifecycle-change.ts │ │ ├── lifecycle-manager.test.ts │ │ ├── lifecycle-manager.ts │ │ └── start-subscription-options.ts │ └── proxy │ │ ├── app-proxy.test.ts │ │ ├── app-proxy.ts │ │ ├── connection-timeout.test.ts │ │ ├── connection-timeout.ts │ │ └── index.ts ├── tsconfig.cjs.json ├── tsconfig.esm.json └── tsconfig.json ├── contact ├── README.md ├── babel.config.js ├── package.json ├── src │ ├── agent-client.test.ts │ ├── agent-client.ts │ ├── contact-client.test.ts │ ├── contact-client.ts │ ├── index.ts │ ├── namespace.ts │ ├── routes.ts │ ├── topic-keys.ts │ └── types.ts ├── tsconfig.cjs.json ├── tsconfig.esm.json └── tsconfig.json ├── core ├── README.md ├── babel.config.js ├── package.json ├── src │ ├── amazon-connect-config.ts │ ├── amazon-connect-error.ts │ ├── amazon-connect-namespace.ts │ ├── client │ │ ├── connect-client-config.ts │ │ ├── connect-client.test.ts │ │ ├── connect-client.ts │ │ └── index.ts │ ├── context │ │ ├── context.test.ts │ │ ├── context.ts │ │ ├── index.ts │ │ ├── module-context.test.ts │ │ └── module-context.ts │ ├── error │ │ ├── connect-error.test.ts │ │ ├── connect-error.ts │ │ └── index.ts │ ├── index.ts │ ├── logging │ │ ├── connect-logger.test.ts │ │ ├── connect-logger.ts │ │ ├── index.ts │ │ ├── log-data-console-writer.test.ts │ │ ├── log-data-console-writer.ts │ │ ├── log-data-transformer.test.ts │ │ ├── log-data-transformer.ts │ │ ├── log-level.ts │ │ ├── log-message-factory.ts │ │ ├── log-provider.ts │ │ ├── log-proxy.ts │ │ ├── logger-types.ts │ │ └── proxy-log-data.ts │ ├── messaging │ │ ├── child-connection-messages.ts │ │ ├── downstream-message-sanitizer.test.ts │ │ ├── downstream-message-sanitizer.ts │ │ ├── index.ts │ │ ├── messages.ts │ │ ├── subscription │ │ │ ├── index.ts │ │ │ ├── subscription-handler-id-map.test.ts │ │ │ ├── subscription-handler-id-map.ts │ │ │ ├── subscription-manager.test.ts │ │ │ ├── subscription-manager.ts │ │ │ ├── subscription-map.test.ts │ │ │ ├── subscription-map.ts │ │ │ ├── subscription-set.test.ts │ │ │ ├── subscription-set.ts │ │ │ └── types.ts │ │ └── upstream-message-origin.ts │ ├── metric │ │ ├── connect-metric-recorder.test.ts │ │ ├── connect-metric-recorder.ts │ │ ├── duration-metric-recorder.test.ts │ │ ├── duration-metric-recorder.ts │ │ ├── index.ts │ │ ├── metric-helper.test.ts │ │ ├── metric-helpers.ts │ │ ├── metric-provider.ts │ │ ├── metric-proxy.ts │ │ ├── metric-types.ts │ │ └── proxy-metric-data.ts │ ├── provider │ │ ├── global-provider.test.ts │ │ ├── global-provider.ts │ │ ├── index.ts │ │ ├── provider-base.test.ts │ │ ├── provider-base.ts │ │ └── provider.ts │ ├── proxy │ │ ├── channel-manager.test.ts │ │ ├── channel-manager.ts │ │ ├── error │ │ │ ├── error-service.test.ts │ │ │ ├── error-service.ts │ │ │ └── index.ts │ │ ├── health-check │ │ │ ├── health-check-manager.test.ts │ │ │ ├── health-check-manager.ts │ │ │ ├── health-check-status-changed.ts │ │ │ ├── health-check-status.ts │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── module-proxy-factory.test.ts │ │ ├── module-proxy-factory.ts │ │ ├── module-proxy.ts │ │ ├── proxy-connection │ │ │ ├── index.ts │ │ │ ├── proxy-connection-status-manager.test.ts │ │ │ ├── proxy-connection-status-manager.ts │ │ │ └── types.ts │ │ ├── proxy-factory.ts │ │ ├── proxy-info.ts │ │ ├── proxy-subject-status.ts │ │ ├── proxy.test.ts │ │ └── proxy.ts │ ├── request │ │ ├── client-timeout-error.test.ts │ │ ├── client-timeout-error.ts │ │ ├── handler-not-found-error.ts │ │ ├── index.ts │ │ ├── no-result-error.test.ts │ │ ├── no-result-error.ts │ │ ├── request-handler-factory.test.ts │ │ ├── request-handler-factory.ts │ │ ├── request-handlers.ts │ │ ├── request-manager.test.ts │ │ ├── request-manager.ts │ │ ├── request-message-factory.test.ts │ │ └── request-message-factory.ts │ ├── sdk-version.ts │ └── utility │ │ ├── emitter │ │ ├── async-event-emitter.test.ts │ │ ├── async-event-emitter.ts │ │ ├── emitter-base.ts │ │ ├── emitter.test.ts │ │ ├── emitter.ts │ │ ├── event-emitter.test.ts │ │ ├── event-emitter.ts │ │ └── index.ts │ │ ├── id-generator.test.ts │ │ ├── id-generator.ts │ │ ├── index.ts │ │ ├── location-helper.test.ts │ │ ├── location-helpers.ts │ │ ├── subscription-handler-relay.test.ts │ │ ├── subscription-handler-relay.ts │ │ ├── timeout-tracker.test.ts │ │ └── timeout-tracker.ts ├── tsconfig.cjs.json ├── tsconfig.esm.json └── tsconfig.json ├── email ├── babel.config.js ├── package.json ├── src │ ├── email-client.test.ts │ ├── email-client.ts │ ├── email-namespace.ts │ ├── error │ │ ├── error-helpers.test.ts │ │ ├── error-helpers.ts │ │ └── index.ts │ ├── index.ts │ ├── routes.ts │ ├── topic-keys.ts │ └── types.ts ├── tsconfig.cjs.json ├── tsconfig.esm.json └── tsconfig.json ├── file ├── babel.config.js ├── package.json ├── src │ ├── error │ │ ├── error-helpers.test.ts │ │ ├── error-helpers.ts │ │ └── index.ts │ ├── file-client.test.ts │ ├── file-client.ts │ ├── file-namespace.ts │ ├── index.ts │ ├── routes.ts │ └── types.ts ├── tsconfig.cjs.json ├── tsconfig.esm.json └── tsconfig.json ├── message-template ├── babel.config.js ├── package.json ├── src │ ├── index.ts │ ├── message-template-client.test.ts │ ├── message-template-client.ts │ ├── message-template-namespace.ts │ ├── routes.ts │ └── types.ts ├── tsconfig.cjs.json ├── tsconfig.esm.json └── tsconfig.json ├── package-lock.json ├── package.json ├── quick-responses ├── babel.config.js ├── package.json ├── src │ ├── index.ts │ ├── quick-responses-client.test.ts │ ├── quick-responses-client.ts │ ├── quick-responses-namespace.ts │ ├── routes.ts │ └── types.ts ├── tsconfig.cjs.json ├── tsconfig.esm.json └── tsconfig.json ├── site-streams ├── babel.config.js ├── package.json ├── src │ ├── amazon-connect-streams-site-config.ts │ ├── amazon-connect-streams-site.test.ts │ ├── amazon-connect-streams-site.ts │ ├── index.ts │ ├── streams-site-message-origin.ts │ ├── streams-site-proxy.test.ts │ └── streams-site-proxy.ts ├── tsconfig.cjs.json ├── tsconfig.esm.json └── tsconfig.json ├── site ├── babel.config.js ├── package.json ├── src │ ├── index.ts │ └── proxy │ │ ├── index.ts │ │ ├── site-proxy.test.ts │ │ └── site-proxy.ts ├── tsconfig.cjs.json ├── tsconfig.esm.json └── tsconfig.json ├── theme ├── README.md ├── babel.config.js ├── jest.config.js ├── package.json ├── src │ ├── __snapshots__ │ │ └── theming.test.ts.snap │ ├── build-theme.ts │ ├── connect-constants.ts │ ├── connect-overrides.ts │ ├── dark-mode-values.ts │ ├── index.ts │ ├── merge-overrides.ts │ ├── supported-overrides.ts │ ├── theming.test.ts │ ├── theming.ts │ └── trimUndefinedValues.ts ├── tsconfig.cjs.json ├── tsconfig.esm.json └── tsconfig.json ├── tsconfig.json ├── user ├── README.md ├── babel.config.js ├── package.json ├── src │ ├── index.ts │ ├── namespace.ts │ ├── routes.ts │ ├── settings-client.test.ts │ ├── settings-client.ts │ ├── topic-keys.ts │ └── types.ts ├── tsconfig.cjs.json ├── tsconfig.esm.json └── tsconfig.json ├── voice ├── README.md ├── babel.config.js ├── package.json ├── src │ ├── index.ts │ ├── namespace.ts │ ├── routes.ts │ ├── types.ts │ ├── voice-client.test.ts │ └── voice-client.ts ├── tsconfig.cjs.json ├── tsconfig.esm.json └── tsconfig.json └── workspace-types ├── README.md ├── babel.config.js ├── package.json ├── src ├── app-config.ts ├── app-host-init-message.ts ├── app-message.ts ├── app-scope.ts ├── index.ts ├── lifecycle-stage.ts └── message-origin.ts ├── tsconfig.cjs.json ├── tsconfig.esm.json └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | build/ 2 | node_modules/ 3 | */lib/ 4 | */lib-esm/ 5 | jest.config.js 6 | coverage/ 7 | */babel.config.js 8 | .eslintrc.cjs -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | "eslint:recommended", 4 | "plugin:@typescript-eslint/recommended", 5 | "plugin:@typescript-eslint/recommended-type-checked", 6 | "prettier", 7 | ], 8 | parser: "@typescript-eslint/parser", 9 | parserOptions: { 10 | tsconfigRootDir: __dirname, 11 | project: ["./*/tsconfig.json"], 12 | }, 13 | plugins: ["prettier", "@typescript-eslint", "simple-import-sort"], 14 | root: true, 15 | rules: { 16 | "@typescript-eslint/no-import-type-side-effects": "error", 17 | "simple-import-sort/imports": "error", 18 | "simple-import-sort/exports": "error", 19 | "prettier/prettier": "error", 20 | "arrow-body-style": "off", 21 | "prefer-arrow-callback": "off", 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .npmrc 3 | .cache/ 4 | coverage/ 5 | lib/* 6 | lib-esm/* 7 | **/lib/* 8 | **/lib-esm/* 9 | *.log 10 | .git 11 | build 12 | *.iml 13 | .DS_Store -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx lint-staged -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*.ts": [ 3 | "eslint --fix" 4 | ] 5 | } -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build/ 2 | node_modules/ 3 | lib/ 4 | *.md -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all" 3 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG.md 2 | 3 | ## 1.0.6 4 | ### Updated 5 | - `@amazon-connect/contact` 6 | - `AgentClient`: Fixed issue that may cause errors when invoking `setOffline` 7 | 8 | ## 1.0.5 9 | ### Added 10 | - New APIs to `ContactClient`, `AgentClient` and `VoiceClient` 11 | ### Updated 12 | - Deprecated `getType`,`onDestroyed` and `offDestroyed` from `ContactClient` 13 | - Deprecated `getDialableCountries` from `AgentClient` 14 | - Deprecated `getPhoneNumber` from `VoiceClient` 15 | 16 | ## 1.0.4 17 | ### Added 18 | - Initial release of `@amazon-connect/email`, providing the ability to handle email contacts through the `EmailClient` 19 | - Initial release of `@amazon-connect/file`, providing the ability to handle attachments through the `FileClient` 20 | - Initial release of `@amazon-connect/message-template`, providing the ability to work with message templates through the `MessageTemplateClient` 21 | - Initial release of `@amazon-connect/quick-responses`, providing the ability to search quick responses through the `QuickResponsesClient` 22 | 23 | ### Updated 24 | - `@amazon-connect/contact` 25 | - `AgentClient`: Fixed AgentState and AgentStateChanged type 26 | - `@amazon-connect/user` 27 | - `SettingsClient`: Fixed the getLanguage type and UserLanguageChanged type 28 | - `@amazon-connect/voice` 29 | - Renamed `VoiceRequests` enum to `VoiceRoutes` 30 | 31 | ## 1.0.3 32 | - Initial release of the `user` module 33 | - Added onConnected event in the `contact` module 34 | 35 | ## 1.0.2 36 | - Initial release of the `contact`, `voice` and `theme` modules 37 | 38 | ## 1.0.0 39 | - Initial release of the Amazon Connect SDK, along with the `app`, `core`, and `workspace-types` modules. -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Amazon Connect SDK 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # About 2 | 3 | The Amazon Connect SDK is a set of modules that can be used to achieve deep integrations within the Amazon Connect ecosystem. Notably, the Amazon Connect SDK can be used by Agent Workspace applications within the Agent Workspace. 4 | 5 | # Learn More 6 | 7 | To learn more about developing within the Agent Workspace, please check out the [Agent Workspace developer guide](https://docs.aws.amazon.com/agentworkspace/latest/devguide) 8 | 9 | # Usage 10 | 11 | To install the app module, perform the following command within your node package: 12 | ``` npm install --save @amazon-connect/app ``` 13 | -------------------------------------------------------------------------------- /app/README.md: -------------------------------------------------------------------------------- 1 | # AmazonConnectSDK - app 2 | 3 | This module is required for applications to integrate with the Agent Workspace. It provides core application features like logging, error handling, secure messaging, and lifecycle events. 4 | -------------------------------------------------------------------------------- /app/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ["@babel/preset-env", { targets: { node: "current" } }], 4 | "@babel/preset-typescript", 5 | ], 6 | }; 7 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@amazon-connect/app", 3 | "version": "1.0.6", 4 | "description": "App functionality of the Amazon Connect SDK", 5 | "publishConfig": { 6 | "access": "public" 7 | }, 8 | "scripts": { 9 | "build:esm": "tsc -p tsconfig.esm.json", 10 | "build:cjs": "tsc -p tsconfig.cjs.json", 11 | "build": "npm run build:esm && npm run build:cjs", 12 | "check-types": "tsc --noEmit", 13 | "clean": "rm -rf ./lib ./lib-esm ./build", 14 | "prepack": "npm run build", 15 | "test": "jest --coverage" 16 | }, 17 | "license": "Apache-2.0", 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/amazon-connect/AmazonConnectSDK.git" 21 | }, 22 | "main": "lib/index.js", 23 | "module": "lib-esm/index.js", 24 | "types": "lib/index.d.ts", 25 | "files": [ 26 | "lib/**/*", 27 | "lib-esm/**/*" 28 | ], 29 | "dependencies": { 30 | "@amazon-connect/workspace-types": "1.0.6", 31 | "@amazon-connect/core": "1.0.6" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/amazon-connect-app-config.ts: -------------------------------------------------------------------------------- 1 | import { AmazonConnectConfig } from "@amazon-connect/core"; 2 | 3 | import { AppCreateHandler, AppDestroyHandler } from "./lifecycle"; 4 | 5 | export type AmazonConnectAppConfig = { 6 | onCreate: AppCreateHandler; 7 | onDestroy?: AppDestroyHandler; 8 | 9 | workspace?: { 10 | connectionTimeout?: number; // Number of milliseconds. Defaults to 5000 11 | }; 12 | } & AmazonConnectConfig; 13 | -------------------------------------------------------------------------------- /app/src/amazon-connect-app.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AmazonConnectProviderBase, 3 | ConnectLogData, 4 | ConnectLogger, 5 | getGlobalProvider, 6 | SubscriptionHandler, 7 | SubscriptionHandlerData, 8 | SubscriptionTopic, 9 | } from "@amazon-connect/core"; 10 | 11 | import { AmazonConnectAppConfig } from "./amazon-connect-app-config"; 12 | import { 13 | AppStartHandler, 14 | AppStopHandler, 15 | LifecycleManager, 16 | StartSubscriptionOptions, 17 | } from "./lifecycle"; 18 | import { AppProxy } from "./proxy"; 19 | 20 | export class AmazonConnectApp extends AmazonConnectProviderBase { 21 | private readonly lifecycleManager: LifecycleManager; 22 | private readonly logger: ConnectLogger; 23 | 24 | constructor(config: AmazonConnectAppConfig) { 25 | super({ config, proxyFactory: () => this.createProxy() }); 26 | this.lifecycleManager = new LifecycleManager(this); 27 | this.logger = new ConnectLogger({ provider: this, source: "app.provider" }); 28 | } 29 | 30 | static init(config: AmazonConnectAppConfig): { 31 | provider: AmazonConnectApp; 32 | } { 33 | const provider = new AmazonConnectApp(config); 34 | 35 | AmazonConnectApp.initializeProvider(provider); 36 | 37 | return { provider }; 38 | } 39 | 40 | static get default(): AmazonConnectApp { 41 | return getGlobalProvider( 42 | "AmazonConnectApp has not been initialized", 43 | ); 44 | } 45 | 46 | private createProxy(): AppProxy { 47 | return new AppProxy(this, this.lifecycleManager); 48 | } 49 | 50 | onStart(handler: AppStartHandler, options?: StartSubscriptionOptions): void { 51 | this.lifecycleManager.onStart(handler, options); 52 | } 53 | 54 | onStop(handler: AppStopHandler): void { 55 | this.lifecycleManager.onStop(handler); 56 | } 57 | 58 | offStart(handler: AppStartHandler): void { 59 | this.lifecycleManager.offStart(handler); 60 | } 61 | 62 | offStop(handler: AppStopHandler): void { 63 | this.lifecycleManager.offStop(handler); 64 | } 65 | 66 | sendCloseAppRequest(message?: string): void { 67 | (this.getProxy() as AppProxy).tryCloseApp(message, false); 68 | } 69 | 70 | sendError(message: string, data?: ConnectLogData): void { 71 | this.logger.error(message, data); 72 | } 73 | 74 | sendFatalError( 75 | message: string, 76 | data?: Record | Error, 77 | ): void { 78 | (this.getProxy() as AppProxy).tryCloseApp(message, true, data); 79 | } 80 | 81 | subscribe( 82 | topic: SubscriptionTopic, 83 | handler: SubscriptionHandler, 84 | ): void { 85 | this.getProxy().subscribe(topic, handler); 86 | } 87 | 88 | unsubscribe( 89 | topic: SubscriptionTopic, 90 | handler: SubscriptionHandler, 91 | ): void { 92 | this.getProxy().unsubscribe(topic, handler); 93 | } 94 | 95 | publish( 96 | topic: SubscriptionTopic, 97 | data: THandlerData, 98 | ): void { 99 | (this.getProxy() as AppProxy).publish(topic, data); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /app/src/app-context.ts: -------------------------------------------------------------------------------- 1 | import { Context } from "@amazon-connect/core"; 2 | import { AppConfig, ContactScope } from "@amazon-connect/workspace-types"; 3 | 4 | import { AmazonConnectApp } from "./amazon-connect-app"; 5 | 6 | export class AppContext extends Context { 7 | public readonly appInstanceId: string; 8 | public readonly contactScope?: Readonly; 9 | public readonly appConfig: Readonly; 10 | 11 | constructor( 12 | provider: AmazonConnectApp, 13 | appInstanceId: string, 14 | appConfig: AppConfig, 15 | contactScope?: ContactScope, 16 | ) { 17 | super(provider); 18 | this.appInstanceId = appInstanceId; 19 | this.appConfig = appConfig; 20 | this.contactScope = contactScope; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/contact-scope.ts: -------------------------------------------------------------------------------- 1 | export enum AppContactScope { 2 | CurrentContactId = "CURRENT_CONTACT", 3 | } 4 | -------------------------------------------------------------------------------- /app/src/index.ts: -------------------------------------------------------------------------------- 1 | export { AmazonConnectApp } from "./amazon-connect-app"; 2 | export type { AmazonConnectAppConfig } from "./amazon-connect-app-config"; 3 | export { AppContactScope } from "./contact-scope"; 4 | export * from "./lifecycle"; 5 | -------------------------------------------------------------------------------- /app/src/lifecycle/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./lifecycle-change"; 2 | export { LifecycleManager } from "./lifecycle-manager"; 3 | export type { StartSubscriptionOptions } from "./start-subscription-options"; 4 | -------------------------------------------------------------------------------- /app/src/lifecycle/lifecycle-change.ts: -------------------------------------------------------------------------------- 1 | import { LifecycleStage } from "@amazon-connect/workspace-types"; 2 | 3 | import { AppContext } from "../app-context"; 4 | 5 | type BaseLifecycleStageChangeEvent = { 6 | stage: TStage; 7 | context: AppContext; 8 | }; 9 | 10 | export type AppCreateEvent = BaseLifecycleStageChangeEvent<"create">; 11 | export type AppStartEvent = BaseLifecycleStageChangeEvent<"start">; 12 | export type AppStopEvent = BaseLifecycleStageChangeEvent<"stop">; 13 | export type AppDestroyEvent = BaseLifecycleStageChangeEvent<"destroy">; 14 | 15 | export type LifecycleStageChangeEvent = 16 | | AppCreateEvent 17 | | AppStartEvent 18 | | AppStopEvent 19 | | AppDestroyEvent; 20 | 21 | export type LifecycleStageChangeHandler< 22 | T extends LifecycleStageChangeEvent = LifecycleStageChangeEvent, 23 | > = (evt: T) => Promise; 24 | 25 | export type AppCreateHandler = LifecycleStageChangeHandler; 26 | export type AppStartHandler = LifecycleStageChangeHandler; 27 | export type AppStopHandler = LifecycleStageChangeHandler; 28 | export type AppDestroyHandler = LifecycleStageChangeHandler; 29 | -------------------------------------------------------------------------------- /app/src/lifecycle/start-subscription-options.ts: -------------------------------------------------------------------------------- 1 | export type StartSubscriptionOptions = { 2 | invokeIfRunning?: boolean; 3 | }; 4 | -------------------------------------------------------------------------------- /app/src/proxy/connection-timeout.test.ts: -------------------------------------------------------------------------------- 1 | import { AmazonConnectAppConfig } from "../amazon-connect-app-config"; 2 | import { 3 | defaultValue, 4 | getConnectionTimeout, 5 | maxValue, 6 | minValue, 7 | } from "./connection-timeout"; 8 | 9 | describe("getConnectionTimeout", () => { 10 | test("should use default value when workspace not set in config", () => { 11 | const config: AmazonConnectAppConfig = {} as AmazonConnectAppConfig; 12 | 13 | const result = getConnectionTimeout(config); 14 | 15 | expect(result).toEqual(defaultValue); 16 | }); 17 | 18 | test("should use default value when workspace connectionTimeout set in config", () => { 19 | const config: AmazonConnectAppConfig = { 20 | workspace: {}, 21 | } as AmazonConnectAppConfig; 22 | 23 | const result = getConnectionTimeout(config); 24 | 25 | expect(result).toEqual(defaultValue); 26 | }); 27 | 28 | test("should accept config override of default", () => { 29 | const config: AmazonConnectAppConfig = { 30 | workspace: { connectionTimeout: 11001 }, 31 | } as AmazonConnectAppConfig; 32 | 33 | const result = getConnectionTimeout(config); 34 | 35 | expect(result).toEqual(11001); 36 | }); 37 | 38 | test("should enforce minimum value when value is less than that in config", () => { 39 | const value = minValue - 1000; 40 | const config: AmazonConnectAppConfig = { 41 | workspace: { connectionTimeout: value }, 42 | } as AmazonConnectAppConfig; 43 | 44 | const result = getConnectionTimeout(config); 45 | 46 | expect(result).toEqual(minValue); 47 | }); 48 | 49 | test("should enforce maximum value when value is less than that in config", () => { 50 | const value = maxValue + 1000; 51 | const config: AmazonConnectAppConfig = { 52 | workspace: { connectionTimeout: value }, 53 | } as AmazonConnectAppConfig; 54 | 55 | const result = getConnectionTimeout(config); 56 | 57 | expect(result).toEqual(maxValue); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /app/src/proxy/connection-timeout.ts: -------------------------------------------------------------------------------- 1 | import { AmazonConnectAppConfig } from "../amazon-connect-app-config"; 2 | 3 | export const defaultValue = 5 * 1000; 4 | export const minValue = 1; 5 | export const maxValue = 60 * 1000; 6 | 7 | export function getConnectionTimeout(config: AmazonConnectAppConfig): number { 8 | return Math.max( 9 | 1, 10 | Math.min(60000, config.workspace?.connectionTimeout ?? 5000), 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /app/src/proxy/index.ts: -------------------------------------------------------------------------------- 1 | export { AppProxy } from "./app-proxy"; 2 | -------------------------------------------------------------------------------- /app/tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "CommonJS", 5 | }, 6 | "exclude": ["./src/**/*.test.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /app/tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib-esm", 5 | }, 6 | "exclude": ["./src/**/*.test.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib/", 5 | }, 6 | "include": ["./src/**/*"], 7 | "typeAcquisition": { 8 | "include": ["jest"] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /contact/README.md: -------------------------------------------------------------------------------- 1 | # AmazonConnectSDK - contact 2 | 3 | This module contains APIs to interact with agent and contact. 4 | 5 | -------------------------------------------------------------------------------- /contact/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ["@babel/preset-env", { targets: { node: "current" } }], 4 | "@babel/preset-typescript", 5 | ], 6 | }; 7 | -------------------------------------------------------------------------------- /contact/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@amazon-connect/contact", 3 | "version": "1.0.6", 4 | "description": "Agent functionality of the Amazon Connect SDK", 5 | "publishConfig": { 6 | "access": "public" 7 | }, 8 | "scripts": { 9 | "build:esm": "tsc -p tsconfig.esm.json", 10 | "build:cjs": "tsc -p tsconfig.cjs.json", 11 | "build": "npm run build:esm && npm run build:cjs", 12 | "check-types": "tsc --noEmit", 13 | "clean": "rm -rf ./lib ./lib-esm ./build", 14 | "prepack": "npm run build", 15 | "test": "jest --coverage" 16 | }, 17 | "license": "Apache-2.0", 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/amazon-connect/AmazonConnectSDK.git" 21 | }, 22 | "main": "lib/index.js", 23 | "module": "lib-esm/index.js", 24 | "types": "lib/index.d.ts", 25 | "files": [ 26 | "lib/**/*", 27 | "lib-esm/**/*" 28 | ], 29 | "dependencies": { 30 | "@amazon-connect/core": "1.0.6" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /contact/src/agent-client.ts: -------------------------------------------------------------------------------- 1 | import { ConnectClient, ConnectClientConfig } from "@amazon-connect/core"; 2 | 3 | import { contactNamespace } from "./namespace"; 4 | import { AgentRoutes } from "./routes"; 5 | import { AgentTopicKey } from "./topic-keys"; 6 | import { 7 | AgentChannelConcurrency, 8 | AgentRoutingProfile, 9 | AgentState, 10 | AgentStateChangedHandler, 11 | ListQuickConnectsOptions, 12 | ListQuickConnectsResult, 13 | QueueARN, 14 | SetAvailabilityStateResult, 15 | } from "./types"; 16 | 17 | export class AgentClient extends ConnectClient { 18 | constructor(config?: ConnectClientConfig) { 19 | super(contactNamespace, config); 20 | } 21 | 22 | async getARN(): Promise { 23 | const { ARN } = await this.context.proxy.request<{ 24 | ARN: string; 25 | }>(AgentRoutes.getARN); 26 | 27 | return ARN; 28 | } 29 | 30 | async getName(): Promise { 31 | const { name } = await this.context.proxy.request<{ name: string }>( 32 | AgentRoutes.getName, 33 | ); 34 | 35 | return name; 36 | } 37 | 38 | getState(): Promise { 39 | return this.context.proxy.request(AgentRoutes.getState); 40 | } 41 | 42 | getRoutingProfile(): Promise { 43 | return this.context.proxy.request(AgentRoutes.getRoutingProfile); 44 | } 45 | 46 | getChannelConcurrency(): Promise { 47 | return this.context.proxy.request(AgentRoutes.getChannelConcurrency); 48 | } 49 | 50 | async getExtension(): Promise { 51 | const { extension } = await this.context.proxy.request<{ 52 | extension?: string; 53 | }>(AgentRoutes.getExtension); 54 | 55 | return extension; 56 | } 57 | /** 58 | * @deprecated Use `VoiceClient.listDialableCountries` instead. 59 | */ 60 | async getDialableCountries(): Promise { 61 | const { dialableCountries } = await this.context.proxy.request<{ 62 | dialableCountries: string[]; 63 | }>(AgentRoutes.getDialableCountries); 64 | 65 | return dialableCountries; 66 | } 67 | 68 | onStateChanged(handler: AgentStateChangedHandler): void { 69 | this.context.proxy.subscribe({ key: AgentTopicKey.StateChanged }, handler); 70 | } 71 | 72 | offStateChanged(handler: AgentStateChangedHandler): void { 73 | this.context.proxy.unsubscribe( 74 | { key: AgentTopicKey.StateChanged }, 75 | handler, 76 | ); 77 | } 78 | 79 | setAvailabilityState( 80 | agentStateARN: string, 81 | ): Promise { 82 | return this.context.proxy.request(AgentRoutes.setAvailabilityState, { 83 | agentStateARN, 84 | }); 85 | } 86 | 87 | setAvailabilityStateByName( 88 | agentStateName: string, 89 | ): Promise { 90 | return this.context.proxy.request(AgentRoutes.setAvailabilityStateByName, { 91 | agentStateName, 92 | }); 93 | } 94 | 95 | setOffline(): Promise { 96 | return this.context.proxy.request(AgentRoutes.setOffline, {}); 97 | } 98 | 99 | listAvailabilityStates(): Promise { 100 | return this.context.proxy.request(AgentRoutes.listAvailabilityStates); 101 | } 102 | 103 | listQuickConnects( 104 | queueARNs: QueueARN | QueueARN[], 105 | options?: ListQuickConnectsOptions, 106 | ): Promise { 107 | return this.context.proxy.request(AgentRoutes.listQuickConnects, { 108 | queueARNs, 109 | options, 110 | }); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /contact/src/index.ts: -------------------------------------------------------------------------------- 1 | export { AgentClient } from "./agent-client"; 2 | export { ContactClient } from "./contact-client"; 3 | export { contactNamespace } from "./namespace"; 4 | export * from "./routes"; 5 | export * from "./topic-keys"; 6 | export * from "./types"; 7 | -------------------------------------------------------------------------------- /contact/src/namespace.ts: -------------------------------------------------------------------------------- 1 | export const contactNamespace = "aws.connect.contact"; 2 | -------------------------------------------------------------------------------- /contact/src/routes.ts: -------------------------------------------------------------------------------- 1 | export enum AgentRoutes { 2 | getARN = "agent/getARN", 3 | getName = "agent/getName", 4 | getState = "agent/getState", 5 | getRoutingProfile = "agent/getRoutingProfile", 6 | getChannelConcurrency = "agent/getChannelConcurrency", 7 | getExtension = "agent/getExtension", 8 | getDialableCountries = "agent/getDialableCountries", 9 | setAvailabilityState = "agent/setAvailabilityState", 10 | setAvailabilityStateByName = "agent/setAvailabilityStateByName", 11 | setOffline = "agent/setOffline", 12 | listAvailabilityStates = "agent/listAvailabilityStates", 13 | listQuickConnects = "agent/listQuickConnects", 14 | } 15 | 16 | export enum ContactRoutes { 17 | getAttributes = "contact/getAttributes", 18 | getInitialContactId = "contact/getInitialContactId", 19 | getType = "contact/getType", 20 | getStateDuration = "contact/getStateDuration", 21 | getQueue = "contact/getQueue", 22 | getQueueTimestamp = "contact/getQueueTimestamp", 23 | getDescription = "contact/getDescription", 24 | getReferences = "contact/getReferences", 25 | getChannelType = "contact/getChannelType", 26 | addParticipant = "contact/addParticipant", 27 | transfer = "contact/transfer", 28 | accept = "contact/accept", 29 | clear = "contact/clear", 30 | } 31 | -------------------------------------------------------------------------------- /contact/src/topic-keys.ts: -------------------------------------------------------------------------------- 1 | export enum ContactLifecycleTopicKey { 2 | StartingACW = "contact/acw", 3 | Connected = "contact/connected", 4 | Destroyed = "contact/destroy", 5 | Missed = "contact/missed", 6 | Cleared = "contact/cleared", 7 | } 8 | 9 | export enum AgentTopicKey { 10 | StateChanged = "agent/stateChange", 11 | } 12 | -------------------------------------------------------------------------------- /contact/tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "CommonJS", 5 | }, 6 | "exclude": ["./src/**/*.test.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /contact/tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib-esm", 5 | }, 6 | "exclude": ["./src/**/*.test.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /contact/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib/", 5 | }, 6 | "include": ["./src/**/*"], 7 | "typeAcquisition": { 8 | "include": ["jest"] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /core/README.md: -------------------------------------------------------------------------------- 1 | # AmazonConnectSDK - core 2 | 3 | This module contains core constructs utilized by other `@amazon-connect` modules; under normal circumstances this module should not be installed directly. 4 | -------------------------------------------------------------------------------- /core/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ["@babel/preset-env", { targets: { node: "current" } }], 4 | "@babel/preset-typescript", 5 | ], 6 | }; 7 | -------------------------------------------------------------------------------- /core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@amazon-connect/core", 3 | "version": "1.0.6", 4 | "description": "Core functionality of the Amazon Connect SDK", 5 | "publishConfig": { 6 | "access": "public" 7 | }, 8 | "scripts": { 9 | "build:esm": "tsc -p tsconfig.esm.json", 10 | "build:cjs": "tsc -p tsconfig.cjs.json", 11 | "build": "npm run build:esm && npm run build:cjs", 12 | "check-types": "tsc --noEmit", 13 | "clean": "rm -rf ./lib ./lib-esm ./build", 14 | "prepack": "npm run build", 15 | "test": "jest --coverage" 16 | }, 17 | "license": "Apache-2.0", 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/amazon-connect/AmazonConnectSDK.git" 21 | }, 22 | "main": "lib/index.js", 23 | "module": "lib-esm/index.js", 24 | "types": "lib/index.d.ts", 25 | "files": [ 26 | "lib/**/*", 27 | "lib-esm/**/*" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /core/src/amazon-connect-config.ts: -------------------------------------------------------------------------------- 1 | import { LogLevel } from "./logging"; 2 | 3 | export type AmazonConnectConfig = { 4 | // Defaults to LogLevel.error 5 | logging?: { minLogToConsoleLevel?: LogLevel }; 6 | }; 7 | -------------------------------------------------------------------------------- /core/src/amazon-connect-error.ts: -------------------------------------------------------------------------------- 1 | import { ProxyConnectionStatus, ProxySubjectStatus } from "./proxy"; 2 | 3 | export type AmazonConnectError = { 4 | message: string; 5 | key: string; 6 | isFatal: boolean; 7 | connectionStatus: ProxyConnectionStatus; 8 | proxyStatus: ProxySubjectStatus; 9 | details?: Record; 10 | }; 11 | 12 | export type AmazonConnectErrorHandler = (error: AmazonConnectError) => void; 13 | -------------------------------------------------------------------------------- /core/src/amazon-connect-namespace.ts: -------------------------------------------------------------------------------- 1 | export type AmazonConnectNamespace = string; 2 | -------------------------------------------------------------------------------- /core/src/client/connect-client-config.ts: -------------------------------------------------------------------------------- 1 | import { ModuleContext } from "../context"; 2 | import { AmazonConnectProvider } from "../provider"; 3 | 4 | export type ConnectClientConfig = { 5 | context?: ModuleContext; 6 | provider?: AmazonConnectProvider; 7 | }; 8 | -------------------------------------------------------------------------------- /core/src/client/connect-client.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/unbound-method */ 2 | import { MockedClass } from "jest-mock"; 3 | import { mock } from "jest-mock-extended"; 4 | 5 | import { AmazonConnectNamespace } from "../amazon-connect-namespace"; 6 | import { Context, ModuleContext } from "../context"; 7 | import { AmazonConnectProvider } from "../provider"; 8 | import { ConnectClient } from "./connect-client"; 9 | import { ConnectClientConfig } from "./connect-client-config"; 10 | 11 | jest.mock("../context/context"); 12 | 13 | const ContextMock = Context as MockedClass; 14 | 15 | const testClientNamespace = "test-client-namespace"; 16 | 17 | class TestClient extends ConnectClient { 18 | constructor(config?: ConnectClientConfig) { 19 | super(testClientNamespace, config); 20 | } 21 | 22 | get moduleContext(): ModuleContext { 23 | return this.context; 24 | } 25 | 26 | get moduleNamespace(): AmazonConnectNamespace { 27 | return this.namespace; 28 | } 29 | } 30 | 31 | beforeEach(jest.resetAllMocks); 32 | 33 | describe("when instantiating a ConnectClient", () => { 34 | test("should create client when not including a config", () => { 35 | const moduleContextMock = mock(); 36 | ContextMock.prototype.getModuleContext.mockReturnValue(moduleContextMock); 37 | 38 | const sut = new TestClient(); 39 | 40 | expect(ContextMock).toBeCalledWith(undefined); 41 | expect(ContextMock.mock.instances[0].getModuleContext).toBeCalledWith( 42 | testClientNamespace, 43 | ); 44 | expect(sut.moduleContext).toEqual(moduleContextMock); 45 | expect(sut.moduleNamespace).toEqual(testClientNamespace); 46 | }); 47 | 48 | test("should create client when not including a context or provider in config", () => { 49 | const moduleContextMock = mock(); 50 | const config: ConnectClientConfig = {}; 51 | ContextMock.prototype.getModuleContext.mockReturnValue(moduleContextMock); 52 | 53 | const sut = new TestClient(config); 54 | 55 | expect(ContextMock).toBeCalledWith(undefined); 56 | expect(ContextMock.mock.instances[0].getModuleContext).toBeCalledWith( 57 | testClientNamespace, 58 | ); 59 | expect(sut.moduleContext).toEqual(moduleContextMock); 60 | expect(sut.moduleNamespace).toEqual(testClientNamespace); 61 | }); 62 | 63 | test("should create client when including a provider without a context", () => { 64 | const moduleContextMock = mock(); 65 | const provider = mock(); 66 | const config: ConnectClientConfig = { provider }; 67 | ContextMock.prototype.getModuleContext.mockReturnValue(moduleContextMock); 68 | 69 | const sut = new TestClient(config); 70 | 71 | expect(ContextMock).toBeCalledWith(provider); 72 | expect(ContextMock.mock.instances[0].getModuleContext).toBeCalledWith( 73 | testClientNamespace, 74 | ); 75 | expect(sut.moduleContext).toEqual(moduleContextMock); 76 | expect(sut.moduleNamespace).toEqual(testClientNamespace); 77 | }); 78 | 79 | test("should create client when setting a context", () => { 80 | const moduleContextMock = mock(); 81 | const config: ConnectClientConfig = { context: moduleContextMock }; 82 | 83 | const sut = new TestClient(config); 84 | 85 | expect(ContextMock).not.toBeCalled(); 86 | expect(sut.moduleContext).toEqual(moduleContextMock); 87 | expect(sut.moduleNamespace).toEqual(testClientNamespace); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /core/src/client/connect-client.ts: -------------------------------------------------------------------------------- 1 | import { AmazonConnectNamespace } from "../amazon-connect-namespace"; 2 | import { Context, ModuleContext } from "../context"; 3 | import { ConnectClientConfig } from "./connect-client-config"; 4 | 5 | export abstract class ConnectClient { 6 | protected readonly context: ModuleContext; 7 | protected readonly namespace: AmazonConnectNamespace; 8 | 9 | constructor( 10 | namespace: AmazonConnectNamespace, 11 | config: ConnectClientConfig | undefined, 12 | ) { 13 | this.namespace = namespace; 14 | this.context = 15 | config?.context ?? 16 | new Context(config?.provider).getModuleContext(namespace); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /core/src/client/index.ts: -------------------------------------------------------------------------------- 1 | export { ConnectClient } from "./connect-client"; 2 | export { ConnectClientConfig } from "./connect-client-config"; 3 | -------------------------------------------------------------------------------- /core/src/context/context.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/unbound-method */ 2 | import { mock } from "jest-mock-extended"; 3 | 4 | import { ConnectLogger } from "../logging"; 5 | import { ConnectMetricRecorder } from "../metric"; 6 | import { AmazonConnectProvider, getGlobalProvider } from "../provider"; 7 | import { Context } from "./"; 8 | import { ModuleContext } from "./module-context"; 9 | 10 | jest.mock("../logging"); 11 | jest.mock("../metric"); 12 | jest.mock("../provider"); 13 | jest.mock("./module-context"); 14 | 15 | beforeEach(() => { 16 | jest.resetAllMocks(); 17 | }); 18 | 19 | describe("getProxy", () => { 20 | test("should call getProvider().getProxy()", () => { 21 | const testProvider = mock(); 22 | const testContext = new Context(testProvider); 23 | jest.spyOn(testProvider, "getProxy"); 24 | jest.spyOn(testContext, "getProvider"); 25 | 26 | testContext.getProxy(); 27 | 28 | expect(testContext.getProvider).toHaveBeenCalled(); 29 | expect(testProvider.getProxy).toHaveBeenCalled(); 30 | }); 31 | }); 32 | 33 | describe("getProvider", () => { 34 | test("should call getGlobalProvider without this.provider defined", () => { 35 | const testContext = new Context(); 36 | jest.spyOn(testContext, "getProvider"); 37 | 38 | testContext.getProvider(); 39 | 40 | expect(getGlobalProvider).toHaveBeenCalled(); 41 | }); 42 | }); 43 | 44 | describe("getModuleContext", () => { 45 | test("should initialize ModuleContext object", () => { 46 | const testContext = new Context(); 47 | 48 | testContext.getModuleContext("test"); 49 | 50 | expect(ModuleContext).toHaveBeenCalled(); 51 | }); 52 | }); 53 | 54 | describe("createLogger", () => { 55 | test("should initialize ConnectLogger object", () => { 56 | const testContext = new Context(); 57 | 58 | testContext.createLogger("test"); 59 | 60 | expect(ConnectLogger).toHaveBeenCalled(); 61 | }); 62 | }); 63 | 64 | describe("createMetricRecorder", () => { 65 | test("should initialize ConnectMetricRecorder object if param is an object", () => { 66 | const testContext = new Context(); 67 | 68 | testContext.createMetricRecorder({ namespace: "test" }); 69 | 70 | expect(ConnectMetricRecorder).toHaveBeenCalled(); 71 | }); 72 | test("should initialize ConnectMetricRecorder object if param is a string", () => { 73 | const testContext = new Context(); 74 | 75 | testContext.createMetricRecorder("test"); 76 | 77 | expect(ConnectMetricRecorder).toHaveBeenCalled(); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /core/src/context/context.ts: -------------------------------------------------------------------------------- 1 | import { AmazonConnectNamespace } from "../amazon-connect-namespace"; 2 | import { ConnectLogger, ConnectLoggerFromContextParams } from "../logging"; 3 | import { 4 | ConnectMetricRecorder, 5 | ConnectMetricRecorderFromContextParams, 6 | } from "../metric"; 7 | import { AmazonConnectProvider, getGlobalProvider } from "../provider"; 8 | import { Proxy } from "../proxy"; 9 | import { ModuleContext } from "./module-context"; 10 | 11 | export class Context< 12 | TProvider extends AmazonConnectProvider = AmazonConnectProvider, 13 | > { 14 | private readonly provider: TProvider | undefined; 15 | 16 | constructor(provider?: TProvider) { 17 | this.provider = provider; 18 | } 19 | 20 | getProxy(): Proxy { 21 | return this.getProvider().getProxy(); 22 | } 23 | 24 | getModuleContext(moduleNamespace: AmazonConnectNamespace): ModuleContext { 25 | return new ModuleContext(this, moduleNamespace); 26 | } 27 | 28 | getProvider(): TProvider { 29 | if (this.provider) return this.provider; 30 | else return getGlobalProvider(); 31 | } 32 | 33 | createLogger(params: ConnectLoggerFromContextParams): ConnectLogger { 34 | if (typeof params === "object") { 35 | return new ConnectLogger({ 36 | ...params, 37 | provider: () => this.getProvider(), 38 | }); 39 | } else { 40 | return new ConnectLogger({ 41 | source: params, 42 | provider: () => this.getProvider(), 43 | }); 44 | } 45 | } 46 | 47 | createMetricRecorder( 48 | params: ConnectMetricRecorderFromContextParams, 49 | ): ConnectMetricRecorder { 50 | if (typeof params === "object") { 51 | return new ConnectMetricRecorder({ 52 | ...params, 53 | provider: () => this.getProvider(), 54 | }); 55 | } else { 56 | return new ConnectMetricRecorder({ 57 | namespace: params, 58 | provider: () => this.getProvider(), 59 | }); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /core/src/context/index.ts: -------------------------------------------------------------------------------- 1 | export { Context } from "./context"; 2 | export { ModuleContext } from "./module-context"; 3 | -------------------------------------------------------------------------------- /core/src/context/module-context.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-expressions */ 2 | /* eslint-disable @typescript-eslint/unbound-method */ 3 | import * as proxy from "../proxy"; 4 | import { Context, ModuleContext } from "./"; 5 | 6 | beforeEach(() => { 7 | jest.resetAllMocks(); 8 | }); 9 | 10 | jest.mock("../logging"); 11 | jest.mock("./context"); 12 | jest.mock("../proxy"); 13 | 14 | describe("ModuleContext", () => { 15 | test("Multiple get proxy (in ModuleContext scope) calls Context.getProxy() only once", () => { 16 | const testContext = new Context(); 17 | const testNameSpace = "test"; 18 | const testModuleContext = new ModuleContext(testContext, testNameSpace); 19 | jest.spyOn(testContext, "getProxy"); 20 | jest.spyOn(proxy, "createModuleProxy").mockImplementation(() => { 21 | return {} as unknown as proxy.ModuleProxy; 22 | }); 23 | 24 | testModuleContext.proxy; 25 | testModuleContext.proxy; 26 | expect(testContext.getProxy).toHaveBeenCalledTimes(1); 27 | expect(proxy.createModuleProxy).toHaveBeenCalledTimes(1); 28 | }); 29 | 30 | test("createLogger", () => { 31 | const testContext = new Context(); 32 | const testNameSpace = "test"; 33 | const testModuleContext = new ModuleContext(testContext, testNameSpace); 34 | jest.spyOn(testContext, "createLogger"); 35 | 36 | testModuleContext.createLogger("test"); 37 | expect(testContext.createLogger).toHaveBeenCalled(); 38 | }); 39 | 40 | test("createMetricRecorder", () => { 41 | const testContext = new Context(); 42 | const testNameSpace = "test"; 43 | const testModuleContext = new ModuleContext(testContext, testNameSpace); 44 | jest.spyOn(testContext, "createMetricRecorder"); 45 | 46 | testModuleContext.createMetricRecorder({ namespace: "test" }); 47 | expect(testContext.createMetricRecorder).toHaveBeenCalled(); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /core/src/context/module-context.ts: -------------------------------------------------------------------------------- 1 | import { AmazonConnectNamespace } from "../amazon-connect-namespace"; 2 | import { ConnectLogger, ConnectLoggerFromContextParams } from "../logging"; 3 | import { 4 | ConnectMetricRecorder, 5 | ConnectMetricRecorderFromContextParams, 6 | } from "../metric"; 7 | import { createModuleProxy, ModuleProxy } from "../proxy"; 8 | import { Context } from "./context"; 9 | 10 | export class ModuleContext { 11 | constructor( 12 | private readonly engineContext: Context, 13 | private readonly moduleNamespace: AmazonConnectNamespace, 14 | ) {} 15 | 16 | private moduleProxy: ModuleProxy | undefined; 17 | 18 | get proxy(): ModuleProxy { 19 | if (!this.moduleProxy) { 20 | const proxy = this.engineContext.getProxy(); 21 | const moduleNamespace = this.moduleNamespace; 22 | this.moduleProxy = createModuleProxy(proxy, moduleNamespace); 23 | } 24 | return this.moduleProxy; 25 | } 26 | 27 | createLogger(params: ConnectLoggerFromContextParams): ConnectLogger { 28 | return this.engineContext.createLogger(params); 29 | } 30 | 31 | createMetricRecorder( 32 | params: ConnectMetricRecorderFromContextParams, 33 | ): ConnectMetricRecorder { 34 | return this.engineContext.createMetricRecorder(params); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /core/src/error/connect-error.test.ts: -------------------------------------------------------------------------------- 1 | import { ConnectError, isConnectError } from "./connect-error"; 2 | 3 | describe("Connect Error", () => { 4 | test("should have the correct properties with details", () => { 5 | const error = new ConnectError({ 6 | reason: "someReason", 7 | namespace: "test-namespace", 8 | errorKey: "someErrorKey", 9 | details: { 10 | command: "test-command", 11 | requestData: undefined, 12 | }, 13 | }); 14 | 15 | expect(error).toBeInstanceOf(Error); 16 | expect(error).toBeInstanceOf(ConnectError); 17 | expect(error.errorKey).toBe("someErrorKey"); 18 | expect(error.reason).toBe("someReason"); 19 | expect(error.namespace).toBe("test-namespace"); 20 | expect(error.details).toStrictEqual({ 21 | command: "test-command", 22 | requestData: undefined, 23 | }); 24 | expect(error.message).toContain("someErrorKey"); 25 | }); 26 | 27 | test("should have the correct properties without details", () => { 28 | const error = new ConnectError({ 29 | reason: "someReason", 30 | namespace: "test-namespace", 31 | errorKey: "someErrorKey", 32 | }); 33 | 34 | expect(error).toBeInstanceOf(Error); 35 | expect(error).toBeInstanceOf(ConnectError); 36 | expect(error.errorKey).toBe("someErrorKey"); 37 | expect(error.reason).toBe("someReason"); 38 | expect(error.namespace).toBe("test-namespace"); 39 | expect(error.details).toStrictEqual({}); 40 | expect(error.message).toContain("someErrorKey"); 41 | }); 42 | }); 43 | 44 | describe("isConnectError", () => { 45 | test("should return true for module error", () => { 46 | const moduleError = new ConnectError({ 47 | errorKey: "someKey", 48 | reason: "someReason", 49 | namespace: "someNamespace", 50 | }); 51 | 52 | const result = isConnectError(moduleError); 53 | 54 | expect(result).toBeTruthy(); 55 | }); 56 | test("should return true for an error with the same type", () => { 57 | class AltError extends Error { 58 | readonly errorType = ConnectError.ErrorType; 59 | } 60 | const altError = new AltError(); 61 | 62 | const result = isConnectError(altError); 63 | 64 | expect(result).toBeTruthy(); 65 | }); 66 | test("should return false for a different type of error", () => { 67 | const testError = new Error("some error"); 68 | 69 | const result = isConnectError(testError); 70 | 71 | expect(result).toBeFalsy(); 72 | }); 73 | test("should return false for a string", () => { 74 | const result = isConnectError("some error"); 75 | 76 | expect(result).toBeFalsy(); 77 | }); 78 | test("should return false for a null", () => { 79 | const result = isConnectError(null); 80 | 81 | expect(result).toBeFalsy(); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /core/src/error/connect-error.ts: -------------------------------------------------------------------------------- 1 | import { ConnectResponseError } from "../request"; 2 | 3 | export class ConnectError extends Error { 4 | static readonly ErrorType = "ConnectError"; 5 | readonly errorType = ConnectError.ErrorType; 6 | 7 | readonly errorKey: string; 8 | readonly namespace: string | undefined; 9 | readonly details: Record; 10 | readonly reason: string | undefined; 11 | 12 | constructor({ 13 | reason, 14 | namespace, 15 | errorKey, 16 | details, 17 | }: 18 | | { 19 | errorKey: string; 20 | reason?: string; 21 | namespace?: string; 22 | details?: Record; 23 | } 24 | | ConnectResponseError) { 25 | super(`ConnectError with error key "${errorKey}"`); 26 | this.namespace = namespace; 27 | this.errorKey = errorKey; 28 | this.reason = reason; 29 | this.details = details ?? {}; 30 | } 31 | } 32 | 33 | export function isConnectError(error: unknown): error is ConnectError { 34 | return Boolean( 35 | error instanceof ConnectError || 36 | (error && 37 | typeof error === "object" && 38 | "errorType" in error && 39 | error.errorType === ConnectError.ErrorType), 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /core/src/error/index.ts: -------------------------------------------------------------------------------- 1 | export { ConnectError, isConnectError } from "./connect-error"; 2 | -------------------------------------------------------------------------------- /core/src/index.ts: -------------------------------------------------------------------------------- 1 | export type { AmazonConnectConfig } from "./amazon-connect-config"; 2 | export type { 3 | AmazonConnectError, 4 | AmazonConnectErrorHandler, 5 | } from "./amazon-connect-error"; 6 | export type { AmazonConnectNamespace } from "./amazon-connect-namespace"; 7 | export { ConnectClient, ConnectClientConfig } from "./client"; 8 | export { Context, ModuleContext } from "./context"; 9 | export { ConnectError, isConnectError } from "./error"; 10 | export * from "./logging"; 11 | export type { 12 | AcknowledgeMessage, 13 | ChildConnectionCloseMessage, 14 | ChildConnectionEnabledDownstreamMessage, 15 | ChildConnectionEnabledUpstreamMessage, 16 | ChildConnectionReadyMessage, 17 | ChildDownstreamMessage, 18 | ChildUpstreamMessage, 19 | CloseChannelMessage, 20 | DownstreamMessage, 21 | ErrorMessage, 22 | HasUpstreamMessageOrigin, 23 | HealthCheckMessage, 24 | HealthCheckResponseMessage, 25 | LogMessage, 26 | MetricMessage, 27 | PublishMessage, 28 | RequestMessage, 29 | ResponseMessage, 30 | SubscribeMessage, 31 | UnsubscribeMessage, 32 | UpstreamMessage, 33 | UpstreamMessageOrigin, 34 | } from "./messaging"; 35 | export type { 36 | ModuleSubscriptionTopic, 37 | SubscriptionHandler, 38 | SubscriptionHandlerData, 39 | SubscriptionHandlerId, 40 | SubscriptionTopic, 41 | SubscriptionTopicKey, 42 | SubscriptionTopicParameter, 43 | } from "./messaging/subscription"; 44 | export { SubscriptionMap, SubscriptionSet } from "./messaging/subscription"; 45 | export * from "./metric"; 46 | export { 47 | AmazonConnectProvider, 48 | AmazonConnectProviderBase, 49 | getGlobalProvider, 50 | resetGlobalProvider, 51 | } from "./provider"; 52 | export type { 53 | HealthCheckStatus, 54 | HealthCheckStatusChanged, 55 | HealthCheckStatusChangedHandler, 56 | ModuleProxy, 57 | ProxyConnecting, 58 | ProxyConnectionChangedHandler, 59 | ProxyConnectionEvent, 60 | ProxyConnectionStatus, 61 | ProxyError, 62 | ProxyFactory, 63 | ProxyInitializing, 64 | ProxyReady, 65 | ProxySubjectStatus, 66 | } from "./proxy"; 67 | export { createModuleProxy, Proxy } from "./proxy"; 68 | export type { 69 | ConnectRequest, 70 | ConnectRequestData, 71 | ConnectResponse, 72 | ConnectResponseData, 73 | ConnectResponseError, 74 | ConnectResponseSuccess, 75 | HandlerNotFoundResponseError, 76 | HandlerNotFoundResponseErrorType, 77 | NoResultResponseError, 78 | NoResultResponseErrorType, 79 | RequestId, 80 | } from "./request"; 81 | export { 82 | formatClientTimeoutError, 83 | handlerNotFoundResponseErrorKey, 84 | isClientTimeoutResponseError, 85 | isNoResultResponseError, 86 | noResultResponseErrorKey, 87 | } from "./request"; 88 | export * from "./request"; 89 | export { sdkVersion } from "./sdk-version"; 90 | export * from "./utility"; 91 | -------------------------------------------------------------------------------- /core/src/logging/index.ts: -------------------------------------------------------------------------------- 1 | export { ConnectLogger } from "./connect-logger"; 2 | export { logToConsole } from "./log-data-console-writer"; 3 | export { LogLevel } from "./log-level"; 4 | export { createLogMessage } from "./log-message-factory"; 5 | export type { LogProvider } from "./log-provider"; 6 | export type { LogProxy } from "./log-proxy"; 7 | export type * from "./logger-types"; 8 | export type { ProxyLogData } from "./proxy-log-data"; 9 | -------------------------------------------------------------------------------- /core/src/logging/log-data-console-writer.test.ts: -------------------------------------------------------------------------------- 1 | import { logToConsole } from "./log-data-console-writer"; 2 | import { LogLevel } from "./log-level"; 3 | 4 | const testMessage = "hello"; 5 | 6 | beforeEach(() => jest.resetAllMocks()); 7 | 8 | describe("when log entry has data", () => { 9 | const data = { foo: "bar" }; 10 | 11 | test("should log to error level", () => { 12 | const spy = jest.spyOn(global.console, "error").mockImplementation(); 13 | 14 | logToConsole(LogLevel.error, testMessage, data); 15 | 16 | expect(spy).toHaveBeenCalledWith(testMessage, data); 17 | }); 18 | 19 | test("should log to warn level", () => { 20 | const spy = jest.spyOn(global.console, "warn").mockImplementation(); 21 | 22 | logToConsole(LogLevel.warn, testMessage, data); 23 | 24 | expect(spy).toHaveBeenCalledWith(testMessage, data); 25 | }); 26 | 27 | test("should log to info level", () => { 28 | const spy = jest.spyOn(global.console, "info").mockImplementation(); 29 | 30 | logToConsole(LogLevel.info, testMessage, data); 31 | 32 | expect(spy).toHaveBeenCalledWith(testMessage, data); 33 | }); 34 | 35 | test("should log to debug level", () => { 36 | const spy = jest.spyOn(global.console, "debug").mockImplementation(); 37 | 38 | logToConsole(LogLevel.debug, testMessage, data); 39 | 40 | expect(spy).toHaveBeenCalledWith(testMessage, data); 41 | }); 42 | 43 | test("should log to trace level", () => { 44 | const spy = jest.spyOn(global.console, "trace").mockImplementation(); 45 | 46 | logToConsole(LogLevel.trace, testMessage, data); 47 | 48 | expect(spy).toHaveBeenCalledWith(testMessage, data); 49 | }); 50 | 51 | test("should log to no level", () => { 52 | const spy = jest.spyOn(global.console, "log").mockImplementation(); 53 | 54 | logToConsole(undefined, testMessage, data); 55 | 56 | expect(spy).toHaveBeenCalledWith(testMessage, data); 57 | }); 58 | }); 59 | 60 | describe("when log entry does not data", () => { 61 | test("should log to error level", () => { 62 | const spy = jest.spyOn(global.console, "error").mockImplementation(); 63 | 64 | logToConsole(LogLevel.error, testMessage); 65 | 66 | expect(spy).toHaveBeenCalledWith(testMessage); 67 | }); 68 | 69 | test("should log to warn level", () => { 70 | const spy = jest.spyOn(global.console, "warn").mockImplementation(); 71 | 72 | logToConsole(LogLevel.warn, testMessage); 73 | 74 | expect(spy).toHaveBeenCalledWith(testMessage); 75 | }); 76 | 77 | test("should log to info level", () => { 78 | const spy = jest.spyOn(global.console, "info").mockImplementation(); 79 | 80 | logToConsole(LogLevel.info, testMessage); 81 | 82 | expect(spy).toHaveBeenCalledWith(testMessage); 83 | }); 84 | 85 | test("should log to debug level", () => { 86 | const spy = jest.spyOn(global.console, "debug").mockImplementation(); 87 | 88 | logToConsole(LogLevel.debug, testMessage); 89 | 90 | expect(spy).toHaveBeenCalledWith(testMessage); 91 | }); 92 | 93 | test("should log to trace level", () => { 94 | const spy = jest.spyOn(global.console, "trace").mockImplementation(); 95 | 96 | logToConsole(LogLevel.trace, testMessage); 97 | 98 | expect(spy).toHaveBeenCalledWith(testMessage); 99 | }); 100 | 101 | test("should log to no level", () => { 102 | const spy = jest.spyOn(global.console, "log").mockImplementation(); 103 | 104 | logToConsole(undefined, testMessage); 105 | 106 | expect(spy).toHaveBeenCalledWith(testMessage); 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /core/src/logging/log-data-console-writer.ts: -------------------------------------------------------------------------------- 1 | import { LogLevel } from "./log-level"; 2 | import { ConnectLogData } from "./logger-types"; 3 | 4 | export function logToConsole( 5 | level: LogLevel | undefined, 6 | message: string, 7 | data?: ConnectLogData, 8 | ) { 9 | if (data) { 10 | switch (level) { 11 | case LogLevel.error: 12 | console.error(message, data); 13 | break; 14 | case LogLevel.warn: 15 | console.warn(message, data); 16 | break; 17 | case LogLevel.info: 18 | console.info(message, data); 19 | break; 20 | case LogLevel.debug: 21 | console.debug(message, data); 22 | break; 23 | case LogLevel.trace: 24 | console.trace(message, data); 25 | break; 26 | default: 27 | console.log(message, data); 28 | break; 29 | } 30 | } else { 31 | switch (level) { 32 | case LogLevel.error: 33 | console.error(message); 34 | break; 35 | case LogLevel.warn: 36 | console.warn(message); 37 | break; 38 | case LogLevel.info: 39 | console.info(message); 40 | break; 41 | case LogLevel.debug: 42 | console.debug(message); 43 | break; 44 | case LogLevel.trace: 45 | console.trace(message); 46 | break; 47 | default: 48 | console.log(message); 49 | break; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /core/src/logging/log-data-transformer.test.ts: -------------------------------------------------------------------------------- 1 | import { LogDataTransformer } from "./log-data-transformer"; 2 | import { LogLevel } from "./log-level"; 3 | import { ConnectLogData } from "./logger-types"; 4 | 5 | describe("when a mixin is not set", () => { 6 | test("should output exact data field passed", () => { 7 | const data = { foo: "bar" }; 8 | const sut = new LogDataTransformer(undefined); 9 | 10 | const result = sut.getTransformedData(LogLevel.info, data); 11 | 12 | expect(result).toBe(data); 13 | }); 14 | 15 | test("should output undefined when undefined is passee", () => { 16 | const sut = new LogDataTransformer(undefined); 17 | 18 | const result = sut.getTransformedData(LogLevel.info, undefined); 19 | 20 | expect(result).toBeUndefined(); 21 | }); 22 | }); 23 | 24 | describe("when a mixin is set", () => { 25 | test("should combine mixin an log entry data", () => { 26 | const mixinValue = { foo: 1 }; 27 | const entryData = { bar: 2 }; 28 | const sut = new LogDataTransformer(() => mixinValue); 29 | 30 | const result = sut.getTransformedData(LogLevel.info, entryData); 31 | 32 | expect(result).toEqual({ 33 | foo: 1, 34 | bar: 2, 35 | }); 36 | }); 37 | 38 | test("should apply mixin data when log entry data is null", () => { 39 | const mixinValue = { foo: 1 }; 40 | const sut = new LogDataTransformer(() => mixinValue); 41 | 42 | const result = sut.getTransformedData(LogLevel.info, undefined); 43 | 44 | expect(result).toEqual({ 45 | foo: 1, 46 | }); 47 | }); 48 | 49 | test("should favor mixing data over log entry data", () => { 50 | const mixinValue = { foo: 1 }; 51 | const entryData = { bar: 2, foo: 2 }; 52 | const sut = new LogDataTransformer(() => mixinValue); 53 | 54 | const result = sut.getTransformedData(LogLevel.info, entryData); 55 | 56 | expect(result).toEqual({ 57 | foo: 1, 58 | bar: 2, 59 | }); 60 | }); 61 | 62 | test("should allow dynamic results based upon parameters", () => { 63 | const mixin = jest 64 | .fn() 65 | .mockImplementation((d, l) => ({ 66 | level: l, 67 | original: d, 68 | })); 69 | const level = LogLevel.warn; 70 | const entryData = { bar: 2 }; 71 | const sut = new LogDataTransformer(mixin); 72 | 73 | const result = sut.getTransformedData(level, entryData); 74 | 75 | expect(mixin).toHaveBeenCalledWith(entryData, level); 76 | expect(result).toEqual({ 77 | level, 78 | original: entryData, 79 | bar: 2, 80 | }); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /core/src/logging/log-data-transformer.ts: -------------------------------------------------------------------------------- 1 | import { LogLevel } from "./log-level"; 2 | import { ConnectLogData, ConnectLoggerMixin } from "./logger-types"; 3 | 4 | export class LogDataTransformer { 5 | private readonly mixin: ConnectLoggerMixin | undefined; 6 | 7 | constructor(mixin: ConnectLoggerMixin | undefined) { 8 | this.mixin = mixin; 9 | } 10 | 11 | getTransformedData( 12 | level: LogLevel, 13 | data: ConnectLogData | undefined, 14 | ): ConnectLogData | undefined { 15 | if (!this.mixin) return data; 16 | 17 | return { 18 | ...(data ?? {}), 19 | ...this.mixin(data ?? {}, level), 20 | }; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /core/src/logging/log-level.ts: -------------------------------------------------------------------------------- 1 | export enum LogLevel { 2 | trace = 1, 3 | debug = 2, 4 | info = 3, 5 | warn = 4, 6 | error = 5, 7 | } 8 | -------------------------------------------------------------------------------- /core/src/logging/log-message-factory.ts: -------------------------------------------------------------------------------- 1 | import { LogMessage, UpstreamMessageOrigin } from "../messaging"; 2 | import { ConnectLogData } from "./logger-types"; 3 | import { ProxyLogData } from "./proxy-log-data"; 4 | 5 | export function createLogMessage( 6 | { level, source, message, loggerId, data }: ProxyLogData, 7 | context: Record, 8 | messageOrigin: UpstreamMessageOrigin, 9 | ): LogMessage { 10 | // Sanitize guards against a caller provided data object containing a 11 | // non-cloneable object which will fail if sent through a message channel 12 | const sanitizedData = data 13 | ? (JSON.parse(JSON.stringify(data)) as ConnectLogData) 14 | : undefined; 15 | 16 | return { 17 | type: "log", 18 | level, 19 | time: new Date(), 20 | source, 21 | message, 22 | loggerId, 23 | data: sanitizedData, 24 | context, 25 | messageOrigin, 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /core/src/logging/log-provider.ts: -------------------------------------------------------------------------------- 1 | import { AmazonConnectConfig } from "../amazon-connect-config"; 2 | import { LogProxy } from "./log-proxy"; 3 | 4 | export interface LogProvider { 5 | getProxy(): LogProxy; 6 | get id(): string; 7 | get config(): Readonly; 8 | } 9 | -------------------------------------------------------------------------------- /core/src/logging/log-proxy.ts: -------------------------------------------------------------------------------- 1 | import { ProxyLogData } from "./proxy-log-data"; 2 | 3 | export interface LogProxy { 4 | log({ level, source, message, loggerId, data }: ProxyLogData): void; 5 | } 6 | -------------------------------------------------------------------------------- /core/src/logging/logger-types.ts: -------------------------------------------------------------------------------- 1 | import { LogLevel } from "./log-level"; 2 | import { LogProvider } from "./log-provider"; 3 | 4 | export type ConnectLogData = Record; 5 | export type ConnectLoggerMixin = ( 6 | data: ConnectLogData, 7 | level: LogLevel, 8 | ) => ConnectLogData; 9 | 10 | type BaseLoggerParams = { 11 | source: string; 12 | mixin?: ConnectLoggerMixin; 13 | options?: LoggerOptions; 14 | }; 15 | 16 | export type ConnectLoggerFromContextParams = string | BaseLoggerParams; 17 | 18 | export type ConnectLoggerParams = BaseLoggerParams & { 19 | provider?: LogProvider | (() => LogProvider); 20 | }; 21 | 22 | export type LogEntryOptions = { 23 | duplicateMessageToConsole?: boolean; 24 | remoteIgnore?: boolean; 25 | }; 26 | 27 | export type LoggerOptions = { 28 | minLogToConsoleLevelOverride?: LogLevel; 29 | remoteIgnore?: boolean; 30 | }; 31 | -------------------------------------------------------------------------------- /core/src/logging/proxy-log-data.ts: -------------------------------------------------------------------------------- 1 | import { LogLevel } from "./log-level"; 2 | 3 | export type ProxyLogData = { 4 | level: LogLevel; 5 | source: string; 6 | message: string; 7 | loggerId: string; 8 | data?: Record; 9 | }; 10 | -------------------------------------------------------------------------------- /core/src/messaging/child-connection-messages.ts: -------------------------------------------------------------------------------- 1 | import { ProxySubjectStatus } from "../proxy"; 2 | import { DownstreamMessage, UpstreamMessage } from "./messages"; 3 | 4 | export type ChildConnectionReadyMessage = { 5 | type: "childConnectionReady"; 6 | }; 7 | 8 | export type ChildUpstreamMessage = { 9 | type: "childUpstream"; 10 | sourceProviderId: string; 11 | parentProviderId: string; 12 | connectionId: string; 13 | message: ChildConnectionEnabledUpstreamMessage | ChildConnectionReadyMessage; 14 | }; 15 | 16 | export type ChildConnectionEnabledUpstreamMessage = 17 | | UpstreamMessage 18 | | ChildUpstreamMessage; 19 | 20 | export type ChildDownstreamMessage< 21 | T extends ProxySubjectStatus = ProxySubjectStatus, 22 | > = { 23 | type: "childDownstreamMessage"; 24 | connectionId: string; 25 | targetProviderId: string; 26 | message: ChildConnectionEnabledDownstreamMessage; 27 | }; 28 | 29 | export type ChildConnectionCloseMessage = { 30 | type: "childConnectionClose"; 31 | connectionId: string; 32 | }; 33 | 34 | export type ChildConnectionEnabledDownstreamMessage< 35 | T extends ProxySubjectStatus = ProxySubjectStatus, 36 | > = DownstreamMessage | ChildDownstreamMessage | ChildConnectionCloseMessage; 37 | -------------------------------------------------------------------------------- /core/src/messaging/downstream-message-sanitizer.ts: -------------------------------------------------------------------------------- 1 | import { ChildConnectionEnabledDownstreamMessage } from "./child-connection-messages"; 2 | 3 | export function sanitizeDownstreamMessage( 4 | message: ChildConnectionEnabledDownstreamMessage, 5 | ): Record { 6 | try { 7 | switch (message.type) { 8 | case "acknowledge": 9 | case "error": 10 | case "childConnectionClose": 11 | return message; 12 | case "childDownstreamMessage": 13 | return { 14 | ...message, 15 | message: sanitizeDownstreamMessage(message.message), 16 | }; 17 | case "publish": { 18 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 19 | const { data, ...other } = message; 20 | return { ...other }; 21 | } 22 | case "response": { 23 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 24 | if (message.isError) 25 | return { ...message, details: { command: message.details.command } }; 26 | else { 27 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 28 | const { data, ...other } = message; 29 | return { ...other }; 30 | } 31 | } 32 | default: 33 | return message; 34 | } 35 | } catch (error) { 36 | return { 37 | messageDetails: "error when sanitizing downstream message", 38 | message, 39 | error, 40 | }; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /core/src/messaging/index.ts: -------------------------------------------------------------------------------- 1 | export type * from "./child-connection-messages"; 2 | export { sanitizeDownstreamMessage } from "./downstream-message-sanitizer"; 3 | export type * from "./messages"; 4 | export * from "./upstream-message-origin"; 5 | -------------------------------------------------------------------------------- /core/src/messaging/messages.ts: -------------------------------------------------------------------------------- 1 | import { LogLevel } from "../logging"; 2 | import { ProxySubjectStatus } from "../proxy"; 3 | import { 4 | ConnectRequest, 5 | ConnectRequestData, 6 | ConnectResponse, 7 | } from "../request"; 8 | import { SubscriptionHandlerId, SubscriptionTopic } from "./subscription"; 9 | import { HasUpstreamMessageOrigin } from "./upstream-message-origin"; 10 | 11 | export type RequestMessage = 12 | { 13 | type: "request"; 14 | } & ConnectRequest & 15 | HasUpstreamMessageOrigin; 16 | 17 | export type SubscribeMessage = { 18 | type: "subscribe"; 19 | topic: SubscriptionTopic; 20 | handlerId: SubscriptionHandlerId; 21 | } & HasUpstreamMessageOrigin; 22 | 23 | export type UnsubscribeMessage = { 24 | type: "unsubscribe"; 25 | topic: SubscriptionTopic; 26 | } & HasUpstreamMessageOrigin; 27 | 28 | export type LogMessage = { 29 | type: "log"; 30 | level: LogLevel; 31 | source: string; 32 | time: Date; 33 | message: string; 34 | loggerId: string; 35 | data?: Record; 36 | context: Record; 37 | } & HasUpstreamMessageOrigin; 38 | 39 | export type MetricMessage = { 40 | type: "metric"; 41 | namespace: string; 42 | metricName: string; 43 | unit: string; 44 | value: number; 45 | time: Date; 46 | dimensions: Record; 47 | optionalDimensions: Record; 48 | } & HasUpstreamMessageOrigin; 49 | 50 | export type CloseChannelMessage = { 51 | type: "closeChannel"; 52 | }; 53 | 54 | export type HealthCheckMessage = { 55 | type: "healthCheck"; 56 | } & HasUpstreamMessageOrigin; 57 | 58 | export type UpstreamMessage = 59 | | RequestMessage 60 | | SubscribeMessage 61 | | UnsubscribeMessage 62 | | LogMessage 63 | | MetricMessage 64 | | CloseChannelMessage 65 | | HealthCheckMessage; 66 | 67 | export type AcknowledgeMessage< 68 | T extends ProxySubjectStatus = ProxySubjectStatus, 69 | > = { 70 | type: "acknowledge"; 71 | connectionId: string; 72 | status: T; 73 | healthCheckInterval: number; 74 | }; 75 | 76 | export type ErrorMessage = { 77 | type: "error"; 78 | message: string; 79 | key: string; 80 | isFatal: boolean; 81 | status: T; 82 | details?: Record; 83 | }; 84 | 85 | export type ResponseMessage = { 86 | type: "response"; 87 | } & ConnectResponse; 88 | 89 | export type PublishMessage = { 90 | type: "publish"; 91 | topic: SubscriptionTopic; 92 | data: object; 93 | handlerId?: SubscriptionHandlerId; 94 | }; 95 | 96 | export type HealthCheckResponseMessage = { 97 | type: "healthCheckResponse"; 98 | time: number; 99 | counter: number; 100 | }; 101 | 102 | export type DownstreamMessage< 103 | T extends ProxySubjectStatus = ProxySubjectStatus, 104 | > = 105 | | AcknowledgeMessage 106 | | ResponseMessage 107 | | PublishMessage 108 | | ErrorMessage 109 | | HealthCheckResponseMessage; 110 | -------------------------------------------------------------------------------- /core/src/messaging/subscription/index.ts: -------------------------------------------------------------------------------- 1 | export { SubscriptionManager } from "./subscription-manager"; 2 | export { SubscriptionMap } from "./subscription-map"; 3 | export { SubscriptionSet } from "./subscription-set"; 4 | export type { 5 | ModuleSubscriptionTopic, 6 | SubscriptionHandler, 7 | SubscriptionHandlerData, 8 | SubscriptionHandlerId, 9 | SubscriptionHandlerIdMapping, 10 | SubscriptionTopic, 11 | SubscriptionTopicHandlerIdItem, 12 | SubscriptionTopicKey, 13 | SubscriptionTopicParameter, 14 | } from "./types"; 15 | -------------------------------------------------------------------------------- /core/src/messaging/subscription/subscription-handler-id-map.ts: -------------------------------------------------------------------------------- 1 | import { generateUUID } from "../../utility"; 2 | import { 3 | SubscriptionHandler, 4 | SubscriptionHandlerId, 5 | SubscriptionHandlerIdMapping, 6 | } from "./types"; 7 | 8 | export class SubscriptionHandlerIdMap { 9 | private readonly idsByHandler: Map< 10 | SubscriptionHandler, 11 | SubscriptionHandlerId 12 | >; 13 | private readonly handlersById: Map< 14 | SubscriptionHandlerId, 15 | SubscriptionHandler 16 | >; 17 | 18 | constructor() { 19 | this.idsByHandler = new Map(); 20 | this.handlersById = new Map(); 21 | } 22 | 23 | add(handler: SubscriptionHandler): { handlerId: SubscriptionHandlerId } { 24 | const existingId = this.idsByHandler.get(handler); 25 | 26 | if (existingId) { 27 | return { handlerId: existingId }; 28 | } 29 | 30 | const handlerId = generateUUID(); 31 | 32 | this.idsByHandler.set(handler, handlerId); 33 | this.handlersById.set(handlerId, handler); 34 | 35 | return { handlerId }; 36 | } 37 | 38 | getIdByHandler(handler: SubscriptionHandler): SubscriptionHandlerId | null { 39 | return this.idsByHandler.get(handler) ?? null; 40 | } 41 | 42 | getHandlerById(id: SubscriptionHandlerId): SubscriptionHandler | null { 43 | return this.handlersById.get(id) ?? null; 44 | } 45 | 46 | get(): SubscriptionHandlerIdMapping[] { 47 | return [...this.idsByHandler.entries()].map(([handler, handlerId]) => ({ 48 | handler, 49 | handlerId, 50 | })); 51 | } 52 | 53 | delete(handler: SubscriptionHandler): { isEmpty: boolean } { 54 | const handlerId = this.idsByHandler.get(handler); 55 | 56 | if (handlerId) this.handlersById.delete(handlerId); 57 | this.idsByHandler.delete(handler); 58 | 59 | return { isEmpty: this.idsByHandler.size < 1 }; 60 | } 61 | 62 | size(): number { 63 | return this.idsByHandler.size; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /core/src/messaging/subscription/subscription-manager.ts: -------------------------------------------------------------------------------- 1 | import { SubscriptionHandlerIdMap } from "./subscription-handler-id-map"; 2 | import { SubscriptionMap } from "./subscription-map"; 3 | import { 4 | SubscriptionHandler, 5 | SubscriptionHandlerId, 6 | SubscriptionHandlerIdMapping, 7 | SubscriptionTopic, 8 | SubscriptionTopicHandlerIdItem, 9 | } from "./types"; 10 | 11 | export class SubscriptionManager { 12 | private readonly subscriptions: SubscriptionMap; 13 | 14 | constructor() { 15 | this.subscriptions = new SubscriptionMap(); 16 | } 17 | 18 | add( 19 | topic: SubscriptionTopic, 20 | handler: SubscriptionHandler, 21 | ): { handlerId: SubscriptionHandlerId } { 22 | return this.subscriptions 23 | .getOrAdd(topic, () => new SubscriptionHandlerIdMap()) 24 | .add(handler); 25 | } 26 | 27 | get(topic: SubscriptionTopic): SubscriptionHandlerIdMapping[] { 28 | return this.subscriptions.get(topic)?.get() ?? []; 29 | } 30 | 31 | getById( 32 | topic: SubscriptionTopic, 33 | handlerId: SubscriptionHandlerId, 34 | ): SubscriptionHandler | null { 35 | return this.subscriptions.get(topic)?.getHandlerById(handlerId) ?? null; 36 | } 37 | 38 | delete(topic: SubscriptionTopic, handler: SubscriptionHandler): void { 39 | if (this.subscriptions.get(topic)?.delete(handler).isEmpty ?? false) { 40 | this.subscriptions.delete(topic); 41 | } 42 | } 43 | 44 | size(topic: SubscriptionTopic): number { 45 | return this.subscriptions.get(topic)?.size() ?? 0; 46 | } 47 | 48 | isEmpty(topic: SubscriptionTopic): boolean { 49 | return this.size(topic) === 0; 50 | } 51 | 52 | getAllSubscriptions(): SubscriptionTopic[] { 53 | return this.subscriptions.getAllSubscriptions(); 54 | } 55 | 56 | getAllSubscriptionHandlerIds(): SubscriptionTopicHandlerIdItem[] { 57 | return this.subscriptions 58 | .getAllSubscriptions() 59 | .reduce( 60 | (acc, topic) => 61 | acc.concat( 62 | this.get(topic).map(({ handlerId }) => ({ 63 | topic, 64 | handlerId, 65 | })), 66 | ), 67 | [], 68 | ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /core/src/messaging/subscription/subscription-set.test.ts: -------------------------------------------------------------------------------- 1 | import { SubscriptionSet } from "./subscription-set"; 2 | import { SubscriptionTopic } from "./types"; 3 | 4 | const topic1: SubscriptionTopic = { namespace: "foo", key: "a" }; 5 | const topic2: SubscriptionTopic = { namespace: "bar", key: "b" }; 6 | 7 | let sut: SubscriptionSet; 8 | 9 | beforeEach(() => { 10 | sut = new SubscriptionSet(); 11 | }); 12 | 13 | test("should add a value", () => { 14 | const expected = 1; 15 | 16 | sut.add(topic1, expected); 17 | 18 | expect(sut.size(topic1)).toEqual(1); 19 | expect(sut.isEmpty(topic1)).toBeFalsy(); 20 | expect(sut.get(topic1)).toContain(expected); 21 | }); 22 | 23 | test("should get an empty array when attempting to get a topic not defined", () => { 24 | const results = sut.get(topic1); 25 | 26 | expect(results).toHaveLength(0); 27 | }); 28 | 29 | test("should only have one value when same value is added multiple times", () => { 30 | const expected1 = 1; 31 | const expected2 = 2; 32 | 33 | sut.add(topic1, expected1); 34 | sut.add(topic1, expected2); 35 | sut.add(topic1, expected1); 36 | sut.add(topic1, expected2); 37 | 38 | expect(sut.size(topic1)).toEqual(2); 39 | expect(sut.isEmpty(topic1)).toBeFalsy(); 40 | const results = sut.get(topic1); 41 | expect(results).toContain(expected1); 42 | expect(results).toContain(expected2); 43 | }); 44 | 45 | test("should delete value", () => { 46 | const valueToDelete = 1; 47 | const expected = 2; 48 | 49 | sut.add(topic1, valueToDelete); 50 | sut.add(topic1, expected); 51 | sut.delete(topic1, valueToDelete); 52 | 53 | expect(sut.size(topic1)).toEqual(1); 54 | expect(sut.isEmpty(topic1)).toBeFalsy(); 55 | expect(sut.get(topic1)).toContain(expected); 56 | }); 57 | 58 | test("should be empty when all values in set are deleted", () => { 59 | const v1 = 1; 60 | const v2 = 2; 61 | sut.add(topic1, v1); 62 | sut.add(topic1, v2); 63 | sut.delete(topic1, v1); 64 | sut.delete(topic1, v2); 65 | 66 | expect(sut.isEmpty(topic1)).toBeTruthy(); 67 | expect(sut.size(topic1)).toEqual(0); 68 | }); 69 | 70 | test("should be empty when no values were ever added to set", () => { 71 | expect(sut.isEmpty(topic1)).toBeTruthy(); 72 | expect(sut.size(topic1)).toEqual(0); 73 | }); 74 | 75 | test("should be empty when an item was deleted without first being added", () => { 76 | sut.delete(topic1, 1); 77 | sut.add(topic2, 1); 78 | 79 | expect(sut.size(topic1)).toEqual(0); 80 | expect(sut.isEmpty(topic1)).toBeTruthy(); 81 | }); 82 | 83 | test("should not be impacted when deleting a value with a different topic", () => { 84 | const expected = 1; 85 | 86 | sut.add(topic1, expected); 87 | sut.delete(topic2, expected); 88 | 89 | expect(sut.size(topic1)).toEqual(1); 90 | expect(sut.isEmpty(topic1)).toBeFalsy(); 91 | expect(sut.get(topic1)).toContain(expected); 92 | }); 93 | 94 | test("should return subscription when a value is in set", () => { 95 | const valueToDelete = 1; 96 | const expected = 2; 97 | sut.add(topic1, valueToDelete); 98 | sut.add(topic1, expected); 99 | sut.delete(topic1, valueToDelete); 100 | 101 | const result = sut.getAllSubscriptions(); 102 | 103 | expect(result).toHaveLength(1); 104 | expect(result).toContainEqual(expect.objectContaining(topic1)); 105 | }); 106 | 107 | test("should not return subscription when all values is in set are removed", () => { 108 | const v1 = 1; 109 | const v2 = 2; 110 | sut.add(topic1, v1); 111 | sut.add(topic1, v2); 112 | sut.delete(topic1, v1); 113 | sut.delete(topic1, v2); 114 | 115 | const result = sut.getAllSubscriptions(); 116 | 117 | expect(result).toHaveLength(0); 118 | }); 119 | -------------------------------------------------------------------------------- /core/src/messaging/subscription/subscription-set.ts: -------------------------------------------------------------------------------- 1 | import { SubscriptionMap } from "./subscription-map"; 2 | import { SubscriptionTopic } from "./types"; 3 | 4 | export class SubscriptionSet { 5 | private readonly map: SubscriptionMap> = new SubscriptionMap(); 6 | 7 | add(topic: SubscriptionTopic, value: T): void { 8 | this.map.addOrUpdate( 9 | topic, 10 | () => new Set([value]), 11 | (s) => s.add(value), 12 | ); 13 | } 14 | 15 | delete(topic: SubscriptionTopic, value: T): void { 16 | const s = this.map.get(topic); 17 | 18 | if (s) { 19 | s.delete(value); 20 | 21 | if (s.size === 0) { 22 | this.map.delete(topic); 23 | } 24 | } 25 | } 26 | 27 | get(topic: SubscriptionTopic): T[] { 28 | return [...(this.map.get(topic) ?? [])]; 29 | } 30 | 31 | size(topic: SubscriptionTopic): number { 32 | return this.map.get(topic)?.size ?? 0; 33 | } 34 | 35 | isEmpty(topic: SubscriptionTopic): boolean { 36 | return this.size(topic) === 0; 37 | } 38 | 39 | getAllSubscriptions(): SubscriptionTopic[] { 40 | return this.map.getAllSubscriptions(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /core/src/messaging/subscription/types.ts: -------------------------------------------------------------------------------- 1 | import { AmazonConnectNamespace } from "../../amazon-connect-namespace"; 2 | 3 | export type SubscriptionTopicKey = string; 4 | export type SubscriptionTopicParameter = string; 5 | 6 | export type SubscriptionTopic = { 7 | namespace: AmazonConnectNamespace; 8 | key: SubscriptionTopicKey; 9 | parameter?: SubscriptionTopicParameter; 10 | }; 11 | 12 | export type ModuleSubscriptionTopic = Omit; 13 | 14 | export type SubscriptionHandlerData = object; 15 | 16 | export type SubscriptionHandler< 17 | T extends SubscriptionHandlerData = SubscriptionHandlerData, 18 | > = (evt: T) => Promise; 19 | 20 | export type SubscriptionHandlerId = string; 21 | export type SubscriptionHandlerIdMapping = { 22 | handler: SubscriptionHandler; 23 | handlerId: SubscriptionHandlerId; 24 | }; 25 | 26 | export type SubscriptionTopicHandlerIdItem = { 27 | topic: SubscriptionTopic; 28 | handlerId: SubscriptionHandlerId; 29 | }; 30 | -------------------------------------------------------------------------------- /core/src/messaging/upstream-message-origin.ts: -------------------------------------------------------------------------------- 1 | export interface UpstreamMessageOrigin { 2 | _type: string; 3 | providerId: string; 4 | } 5 | 6 | export interface HasUpstreamMessageOrigin { 7 | messageOrigin: UpstreamMessageOrigin; 8 | } 9 | -------------------------------------------------------------------------------- /core/src/metric/duration-metric-recorder.test.ts: -------------------------------------------------------------------------------- 1 | import { DurationMetricRecorder } from "./duration-metric-recorder"; 2 | 3 | const mockAction = jest.fn(); 4 | const testMetricName = "dummy-metric-name"; 5 | const testDimensions = { name1: "value1", name2: "value2" }; 6 | 7 | jest.useFakeTimers(); 8 | 9 | beforeEach(jest.resetAllMocks); 10 | 11 | describe("stopDurationCounter", () => { 12 | beforeEach(() => { 13 | const currentTime = 1500; 14 | jest.setSystemTime(currentTime); 15 | }); 16 | 17 | test("should call sendMetric, when dimension and optionalDimensions are set", () => { 18 | const sut = new DurationMetricRecorder({ 19 | sendMetric: mockAction, 20 | metricName: testMetricName, 21 | metricOptions: { 22 | dimensions: testDimensions, 23 | optionalDimensions: testDimensions, 24 | }, 25 | }); 26 | jest.advanceTimersByTime(100); 27 | 28 | sut.stopDurationCounter(); 29 | 30 | expect(mockAction).toHaveBeenCalledWith({ 31 | metricName: testMetricName, 32 | unit: "Milliseconds", 33 | value: 100, 34 | dimensions: testDimensions, 35 | optionalDimensions: testDimensions, 36 | }); 37 | }); 38 | 39 | test("should call sendMetric, when dimension and optionalDimensions are not set", () => { 40 | const sut = new DurationMetricRecorder({ 41 | sendMetric: mockAction, 42 | metricName: testMetricName, 43 | }); 44 | jest.advanceTimersByTime(100); 45 | 46 | sut.stopDurationCounter(); 47 | 48 | expect(mockAction).toHaveBeenCalledWith({ 49 | metricName: testMetricName, 50 | unit: "Milliseconds", 51 | value: 100, 52 | dimensions: {}, 53 | optionalDimensions: {}, 54 | }); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /core/src/metric/duration-metric-recorder.ts: -------------------------------------------------------------------------------- 1 | import { MetricData, MetricOptions, Unit } from "./metric-types"; 2 | 3 | /** 4 | * @classdesc DurationMetricRecorder class provides APIs to emit duration metrics based on users' need 5 | */ 6 | export class DurationMetricRecorder { 7 | private readonly sendMetric: (metric: MetricData) => void; 8 | private readonly startTime: number; 9 | private readonly metricName: string; 10 | private readonly unit: Unit = "Milliseconds"; 11 | private readonly dimensions: Record; 12 | private readonly optionalDimensions: Record; 13 | 14 | /** 15 | * Constructor for DurationMetricRecorder 16 | * @param {(metric: MetricData) => void} sendMetric- The method that sends metric 17 | * @param {string} metricName - The name of the duration metric 18 | * @param {Record} dimensions - The dimensions of the duration metric with keys and values (optional) 19 | * @param {Record} optionalDimensions - The optional dimensions of the duration metric with keys and values (optional) 20 | */ 21 | constructor({ 22 | sendMetric, 23 | metricName, 24 | metricOptions, 25 | }: { 26 | sendMetric: (metric: MetricData) => void; 27 | metricName: string; 28 | metricOptions?: MetricOptions; 29 | }) { 30 | this.sendMetric = sendMetric; 31 | this.startTime = performance.now(); 32 | this.metricName = metricName; 33 | this.dimensions = metricOptions?.dimensions ? metricOptions.dimensions : {}; 34 | this.optionalDimensions = metricOptions?.optionalDimensions 35 | ? metricOptions.optionalDimensions 36 | : {}; 37 | } 38 | 39 | /** 40 | * Stop recording of the duration metric and emit it 41 | * @returns {durationCount: number} - The duration being recorded 42 | */ 43 | stopDurationCounter(): { duration: number } { 44 | const durationResult = Math.round(performance.now() - this.startTime); 45 | this.sendMetric({ 46 | metricName: this.metricName, 47 | unit: this.unit, 48 | value: durationResult, 49 | dimensions: this.dimensions, 50 | optionalDimensions: this.optionalDimensions, 51 | }); 52 | return { duration: durationResult }; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /core/src/metric/index.ts: -------------------------------------------------------------------------------- 1 | export { ConnectMetricRecorder } from "./connect-metric-recorder"; 2 | export { DurationMetricRecorder } from "./duration-metric-recorder"; 3 | export { createMetricMessage } from "./metric-helpers"; 4 | export { MetricProvider } from "./metric-provider"; 5 | export { MetricProxy } from "./metric-proxy"; 6 | export * from "./metric-types"; 7 | export { ProxyMetricData } from "./proxy-metric-data"; 8 | -------------------------------------------------------------------------------- /core/src/metric/metric-helper.test.ts: -------------------------------------------------------------------------------- 1 | import { mock } from "jest-mock-extended"; 2 | 3 | import { MetricMessage, UpstreamMessageOrigin } from ".."; 4 | import { 5 | checkDimensionLength, 6 | createMetricMessage, 7 | MAX_METRIC_DIMENSIONS, 8 | } from "./metric-helpers"; 9 | import { MetricData } from "./metric-types"; 10 | 11 | jest.mock(".."); 12 | 13 | const testMetricName = "dummy-metric-name"; 14 | const testDimensions = { name1: "value1", name2: "value2" }; 15 | const testTimeStamp = new Date("2024-01-01"); 16 | const testSource = "test-metric-recorder"; 17 | const mockMessageOrigin = mock(); 18 | 19 | beforeEach(() => jest.resetAllMocks()); 20 | 21 | describe("checkDimensionlength", () => { 22 | test("should throw error if the sum of dimensions and optional dimensions is exceeding MAX_METRIC_DIMENSIONS", () => { 23 | const invalidDimension: Record = {}; 24 | for (let i = 0; i < MAX_METRIC_DIMENSIONS + 1; i++) { 25 | invalidDimension[`${i}`] = `${i}`; 26 | } 27 | expect(() => { 28 | checkDimensionLength(invalidDimension); 29 | }).toThrowError("Cannot add more than 30 dimensions to a metric"); 30 | }); 31 | 32 | test("should not throw error if the sum of dimensions and optional dimensions is under MAX_METRIC_DIMENSIONS", () => { 33 | const validDimension: Record = {}; 34 | for (let i = 0; i < MAX_METRIC_DIMENSIONS; i++) { 35 | validDimension[`${i}`] = `${i}`; 36 | } 37 | expect(() => { 38 | checkDimensionLength(validDimension); 39 | }).not.toThrowError("Cannot add more than 30 dimensions to a metric"); 40 | }); 41 | }); 42 | 43 | describe("createMetricMessage", () => { 44 | test("should transform message from ProxyMetricData type into MetricMessage type, when dimensions and optionalDimensions are defined", () => { 45 | const testMetricData: MetricData = { 46 | metricName: testMetricName, 47 | unit: "Count", 48 | value: 0, 49 | dimensions: testDimensions, 50 | optionalDimensions: testDimensions, 51 | }; 52 | const resultMetricMessage: MetricMessage = { 53 | type: "metric", 54 | namespace: testSource, 55 | metricName: testMetricName, 56 | unit: "Count", 57 | value: 0, 58 | time: testTimeStamp, 59 | dimensions: testDimensions, 60 | optionalDimensions: testDimensions, 61 | messageOrigin: mockMessageOrigin, 62 | }; 63 | const message = createMetricMessage( 64 | { 65 | metricData: testMetricData, 66 | time: testTimeStamp, 67 | namespace: testSource, 68 | }, 69 | mockMessageOrigin, 70 | ); 71 | 72 | expect(message).toEqual(resultMetricMessage); 73 | }); 74 | test("should transform message from ProxyMetricData type into MetricMessage type, when dimensions and optionalDimensions are NOT defined", () => { 75 | const testMetricData: MetricData = { 76 | metricName: testMetricName, 77 | unit: "Count", 78 | value: 0, 79 | }; 80 | const resultMetricMessage: MetricMessage = { 81 | type: "metric", 82 | metricName: testMetricName, 83 | unit: "Count", 84 | value: 0, 85 | time: testTimeStamp, 86 | namespace: testSource, 87 | dimensions: {}, 88 | optionalDimensions: {}, 89 | messageOrigin: mockMessageOrigin, 90 | }; 91 | const message = createMetricMessage( 92 | { 93 | metricData: testMetricData, 94 | time: testTimeStamp, 95 | namespace: testSource, 96 | }, 97 | mockMessageOrigin, 98 | ); 99 | 100 | expect(message).toEqual(resultMetricMessage); 101 | }); 102 | }); 103 | -------------------------------------------------------------------------------- /core/src/metric/metric-helpers.ts: -------------------------------------------------------------------------------- 1 | import { MetricMessage, UpstreamMessageOrigin } from "../messaging"; 2 | import { ProxyMetricData } from "./proxy-metric-data"; 3 | 4 | export const MAX_METRIC_DIMENSIONS = 30; 5 | 6 | /** 7 | * Check if the the sum of the length of dimensions and optional dimentions is exceeding maximum dimension length acceptable by back-end 8 | * @param {Record} dimensions - The dimensions of the duration metric with keys and values 9 | * @param {Record} optionalDimensions -The optional dimensions of the duration metric with keys and values 10 | */ 11 | export function checkDimensionLength( 12 | dimensions: Record, 13 | optionalDimensions?: Record, 14 | ): void { 15 | if ( 16 | Object.keys(dimensions).length + 17 | Object.keys(optionalDimensions ?? {}).length > 18 | MAX_METRIC_DIMENSIONS 19 | ) { 20 | throw new Error("Cannot add more than 30 dimensions to a metric"); 21 | } 22 | } 23 | 24 | /** 25 | * Transform the metric message into the format acceptable by back-end 26 | * @param {MetricData} metricData - The metric data 27 | * @param {string} timestamp - The timestamp of the metric 28 | * @param {string} namespace - The namespace of the metric 29 | * @param {UpstreamMessageOrigin} messageOrigin - The origin of the metric message 30 | * @return {MetricMessage} - Return a MetricMessage object 31 | */ 32 | export function createMetricMessage( 33 | { metricData, time, namespace }: ProxyMetricData, 34 | messageOrigin: UpstreamMessageOrigin, 35 | ): MetricMessage { 36 | return { 37 | type: "metric", 38 | namespace: namespace, 39 | metricName: metricData.metricName, 40 | unit: metricData.unit, 41 | value: metricData.value, 42 | time: time, 43 | dimensions: metricData.dimensions ?? {}, 44 | optionalDimensions: metricData.optionalDimensions ?? {}, 45 | messageOrigin, 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /core/src/metric/metric-provider.ts: -------------------------------------------------------------------------------- 1 | import { AmazonConnectConfig } from "../amazon-connect-config"; 2 | import { MetricProxy } from "./metric-proxy"; 3 | export interface MetricProvider { 4 | getProxy(): MetricProxy; 5 | get config(): Readonly; 6 | } 7 | -------------------------------------------------------------------------------- /core/src/metric/metric-proxy.ts: -------------------------------------------------------------------------------- 1 | import { ProxyMetricData } from "./proxy-metric-data"; 2 | 3 | export interface MetricProxy { 4 | sendMetric({ metricData, time, namespace }: ProxyMetricData): void; 5 | } 6 | -------------------------------------------------------------------------------- /core/src/metric/metric-types.ts: -------------------------------------------------------------------------------- 1 | import { MetricProvider } from "./metric-provider"; 2 | 3 | type BaseMetricRecorderParams = { 4 | namespace: string; 5 | }; 6 | 7 | export type ConnectMetricRecorderFromContextParams = 8 | | string 9 | | BaseMetricRecorderParams; 10 | 11 | export type ConnectRecorderMetricParams = BaseMetricRecorderParams & { 12 | provider?: MetricProvider | (() => MetricProvider); 13 | }; 14 | 15 | export type MetricData = { 16 | metricName: string; 17 | unit: Unit; 18 | value: number; 19 | dimensions?: Record; 20 | optionalDimensions?: Record; 21 | }; 22 | 23 | export type Unit = "Count" | "Milliseconds"; 24 | 25 | export type MetricOptions = { 26 | dimensions?: Record; 27 | optionalDimensions?: Record; 28 | }; 29 | -------------------------------------------------------------------------------- /core/src/metric/proxy-metric-data.ts: -------------------------------------------------------------------------------- 1 | import { MetricData } from "./metric-types"; 2 | 3 | export type ProxyMetricData = { 4 | metricData: MetricData; 5 | time: Date; 6 | namespace: string; 7 | }; 8 | -------------------------------------------------------------------------------- /core/src/provider/global-provider.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-require-imports */ 2 | /* eslint-disable @typescript-eslint/no-unsafe-call */ 3 | /* eslint-disable @typescript-eslint/no-unsafe-assignment */ 4 | /* eslint-disable @typescript-eslint/no-var-requires */ 5 | import { mock } from "jest-mock-extended"; 6 | 7 | import { AmazonConnectProvider } from "./provider"; 8 | 9 | jest.mock("../utility/id-generator"); 10 | 11 | beforeEach(() => { 12 | jest.resetModules(); 13 | }); 14 | 15 | describe("setGlobalProvider", () => { 16 | test("should throw error when attempting to set a global provider a second time", () => { 17 | const { setGlobalProvider } = require("./global-provider"); 18 | const provider = mock(); 19 | setGlobalProvider(provider); 20 | 21 | try { 22 | setGlobalProvider(provider); 23 | } catch (error: unknown) { 24 | expect(error).toBeInstanceOf(Error); 25 | expect((error as { message: string }).message).toEqual( 26 | "Global Provider is already set", 27 | ); 28 | } 29 | 30 | expect.hasAssertions(); 31 | }); 32 | }); 33 | 34 | describe("resetGlobalProvider", () => { 35 | describe("when provider is set", () => { 36 | test("test previous should set the provider", () => { 37 | const { 38 | resetGlobalProvider, 39 | getGlobalProvider, 40 | setGlobalProvider, 41 | } = require("./global-provider"); 42 | const originalProvider = mock(); 43 | const newProvider = mock(); 44 | setGlobalProvider(originalProvider); 45 | 46 | resetGlobalProvider(newProvider); 47 | 48 | expect(getGlobalProvider()).toEqual(newProvider); 49 | }); 50 | }); 51 | 52 | describe("when previous provider is not set", () => { 53 | test("test should set the provider", () => { 54 | const { 55 | resetGlobalProvider, 56 | getGlobalProvider, 57 | } = require("./global-provider"); 58 | 59 | const provider = mock(); 60 | 61 | resetGlobalProvider(provider); 62 | 63 | expect(getGlobalProvider()).toEqual(provider); 64 | }); 65 | }); 66 | }); 67 | 68 | describe("getGlobalProvider", () => { 69 | test("should get provider it after it is set", () => { 70 | const { 71 | setGlobalProvider, 72 | getGlobalProvider, 73 | } = require("./global-provider"); 74 | const provider = mock(); 75 | setGlobalProvider(provider); 76 | 77 | const result = getGlobalProvider(provider); 78 | 79 | expect(result).toEqual(provider); 80 | }); 81 | 82 | test("should throw if attempting to get before set", () => { 83 | const { getGlobalProvider } = require("./global-provider"); 84 | 85 | try { 86 | getGlobalProvider(); 87 | } catch (error) { 88 | expect(error).toBeInstanceOf(Error); 89 | } 90 | 91 | expect.hasAssertions(); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /core/src/provider/global-provider.ts: -------------------------------------------------------------------------------- 1 | import { AmazonConnectProvider } from "./provider"; 2 | 3 | let _provider: AmazonConnectProvider | undefined; 4 | 5 | export function setGlobalProvider(provider: AmazonConnectProvider): void { 6 | if (_provider) throw new Error("Global Provider is already set"); 7 | 8 | _provider = provider; 9 | } 10 | 11 | export function resetGlobalProvider(provider: AmazonConnectProvider): void { 12 | _provider = provider; 13 | } 14 | 15 | export function getGlobalProvider< 16 | TProvider extends AmazonConnectProvider = AmazonConnectProvider, 17 | >(notSetMessage?: string): TProvider { 18 | if (!_provider) { 19 | throw new Error( 20 | notSetMessage ?? 21 | "Attempted to get Global AmazonConnectProvider that has not been set.", 22 | ); 23 | } 24 | 25 | return _provider as TProvider; 26 | } 27 | -------------------------------------------------------------------------------- /core/src/provider/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | getGlobalProvider, 3 | resetGlobalProvider, 4 | setGlobalProvider, 5 | } from "./global-provider"; 6 | export type { AmazonConnectProvider } from "./provider"; 7 | export type { AmazonConnectProviderParams } from "./provider-base"; 8 | export { AmazonConnectProviderBase } from "./provider-base"; 9 | -------------------------------------------------------------------------------- /core/src/provider/provider-base.ts: -------------------------------------------------------------------------------- 1 | import { AmazonConnectConfig } from "../amazon-connect-config"; 2 | import { AmazonConnectErrorHandler } from "../amazon-connect-error"; 3 | import { ConnectError } from "../error"; 4 | import { ConnectLogger } from "../logging"; 5 | import { Proxy, ProxyFactory } from "../proxy"; 6 | import { generateUUID } from "../utility"; 7 | import { getGlobalProvider, setGlobalProvider } from "./global-provider"; 8 | import { AmazonConnectProvider } from "./provider"; 9 | 10 | export type AmazonConnectProviderParams = { 11 | config: TConfig; 12 | proxyFactory: ProxyFactory>; 13 | }; 14 | 15 | export class AmazonConnectProviderBase< 16 | TConfig extends AmazonConnectConfig = AmazonConnectConfig, 17 | > implements AmazonConnectProvider 18 | { 19 | private readonly _id: string; 20 | private readonly proxyFactory: ProxyFactory; 21 | private readonly _config: TConfig; 22 | private proxy: Proxy | undefined; 23 | 24 | constructor({ config, proxyFactory }: AmazonConnectProviderParams) { 25 | this._id = generateUUID(); 26 | 27 | if (!proxyFactory) { 28 | throw new Error("Attempted to get Proxy before setting up factory"); 29 | } 30 | 31 | if (!config) { 32 | throw new Error("Failed to include config"); 33 | } 34 | 35 | this.proxyFactory = proxyFactory; 36 | this._config = config; 37 | } 38 | 39 | get id(): string { 40 | return this._id; 41 | } 42 | 43 | getProxy(): Proxy { 44 | if (!this.proxy) { 45 | this.proxy = this.proxyFactory(this); 46 | 47 | this.proxy.init(); 48 | } 49 | 50 | return this.proxy; 51 | } 52 | 53 | get config(): Readonly { 54 | return { ...this._config }; 55 | } 56 | 57 | onError(handler: AmazonConnectErrorHandler): void { 58 | this.getProxy().onError(handler); 59 | } 60 | 61 | offError(handler: AmazonConnectErrorHandler): void { 62 | this.getProxy().offError(handler); 63 | } 64 | 65 | protected static isInitialized = false; 66 | 67 | protected static initializeProvider< 68 | TProvider extends AmazonConnectProviderBase, 69 | >(provider: TProvider): TProvider { 70 | if (this.isInitialized) { 71 | const msg = "Attempted to initialize provider more than one time."; 72 | const details: Record = {}; 73 | 74 | try { 75 | // Attempts to get the existing provider for logging 76 | const existingProvider = getGlobalProvider(); 77 | 78 | const logger = new ConnectLogger({ 79 | source: "core.amazonConnectProvider.init", 80 | provider: existingProvider, 81 | }); 82 | 83 | logger.error(msg); 84 | } catch (e) { 85 | // In the event of a error when logging or attempting 86 | // to get provider when logging, capture the message 87 | // in the error being thrown 88 | details.loggingError = (e as Error)?.message; 89 | } 90 | 91 | throw new ConnectError({ 92 | errorKey: "attemptInitializeMultipleProviders", 93 | reason: msg, 94 | details, 95 | }); 96 | } 97 | 98 | setGlobalProvider(provider); 99 | 100 | this.isInitialized = true; 101 | 102 | // Getting the proxy sets up the connection with subject 103 | provider.getProxy(); 104 | 105 | return provider; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /core/src/provider/provider.ts: -------------------------------------------------------------------------------- 1 | import { AmazonConnectConfig } from "../amazon-connect-config"; 2 | import { AmazonConnectErrorHandler } from "../amazon-connect-error"; 3 | import { Proxy } from "../proxy"; 4 | 5 | export interface AmazonConnectProvider< 6 | TConfig extends AmazonConnectConfig = AmazonConnectConfig, 7 | > { 8 | get id(): string; 9 | get config(): Readonly; 10 | 11 | getProxy(): Proxy; 12 | 13 | onError(handler: AmazonConnectErrorHandler): void; 14 | offError(handler: AmazonConnectErrorHandler): void; 15 | } 16 | -------------------------------------------------------------------------------- /core/src/proxy/error/error-service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AmazonConnectError, 3 | AmazonConnectErrorHandler, 4 | } from "../../amazon-connect-error"; 5 | import { ConnectLogger, LogProvider } from "../../logging"; 6 | 7 | export class ErrorService { 8 | private readonly errorHandlers: Set; 9 | private readonly logger: ConnectLogger; 10 | 11 | constructor(provider: LogProvider) { 12 | this.errorHandlers = new Set(); 13 | this.logger = new ConnectLogger({ 14 | provider, 15 | source: "core.proxy.error", 16 | }); 17 | } 18 | 19 | invoke(error: AmazonConnectError): void { 20 | const { message, key, details, isFatal, connectionStatus } = error; 21 | 22 | this.logger.error( 23 | message, 24 | { 25 | key, 26 | details, 27 | isFatal, 28 | connectionStatus, 29 | }, 30 | { duplicateMessageToConsole: true, remoteIgnore: true }, 31 | ); 32 | 33 | [...this.errorHandlers].forEach((handler) => { 34 | try { 35 | handler(error); 36 | } catch (handlerError) { 37 | this.logger.error( 38 | "An error occurred within a AmazonConnectErrorHandler", 39 | { 40 | handlerError, 41 | originalError: error, 42 | }, 43 | ); 44 | } 45 | }); 46 | } 47 | 48 | onError(handler: AmazonConnectErrorHandler): void { 49 | this.errorHandlers.add(handler); 50 | } 51 | 52 | offError(handler: AmazonConnectErrorHandler): void { 53 | this.errorHandlers.delete(handler); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /core/src/proxy/error/index.ts: -------------------------------------------------------------------------------- 1 | export { ErrorService } from "./error-service"; 2 | -------------------------------------------------------------------------------- /core/src/proxy/health-check/health-check-status-changed.ts: -------------------------------------------------------------------------------- 1 | import { SubscriptionHandler } from "../../messaging/subscription"; 2 | import { HealthCheckStatus } from "./health-check-status"; 3 | 4 | export type HealthCheckStatusChanged = { 5 | status: Exclude; 6 | previousStatus: HealthCheckStatus; 7 | lastCheckTime: number | null; 8 | lastCheckCounter: number | null; 9 | }; 10 | 11 | export type HealthCheckStatusChangedHandler = 12 | SubscriptionHandler; 13 | -------------------------------------------------------------------------------- /core/src/proxy/health-check/health-check-status.ts: -------------------------------------------------------------------------------- 1 | export type HealthCheckStatus = "healthy" | "unhealthy" | "unknown"; 2 | -------------------------------------------------------------------------------- /core/src/proxy/health-check/index.ts: -------------------------------------------------------------------------------- 1 | export type { HealthCheckManagerParams } from "./health-check-manager"; 2 | export { HealthCheckManager } from "./health-check-manager"; 3 | export type { HealthCheckStatus } from "./health-check-status"; 4 | export type { 5 | HealthCheckStatusChanged, 6 | HealthCheckStatusChangedHandler, 7 | } from "./health-check-status-changed"; 8 | -------------------------------------------------------------------------------- /core/src/proxy/index.ts: -------------------------------------------------------------------------------- 1 | export type { 2 | HealthCheckStatus, 3 | HealthCheckStatusChanged, 4 | HealthCheckStatusChangedHandler, 5 | } from "./health-check"; 6 | export type { ModuleProxy } from "./module-proxy"; 7 | export { createModuleProxy } from "./module-proxy-factory"; 8 | export { Proxy } from "./proxy"; 9 | export type { 10 | ProxyConnecting, 11 | ProxyConnectionChangedHandler, 12 | ProxyConnectionEvent, 13 | ProxyConnectionStatus, 14 | ProxyError, 15 | ProxyInitializing, 16 | ProxyReady, 17 | } from "./proxy-connection"; 18 | export type { ProxyFactory } from "./proxy-factory"; 19 | export type { ProxySubjectStatus } from "./proxy-subject-status"; 20 | -------------------------------------------------------------------------------- /core/src/proxy/module-proxy-factory.ts: -------------------------------------------------------------------------------- 1 | import { AmazonConnectNamespace } from "../amazon-connect-namespace"; 2 | import { ModuleProxy } from "./module-proxy"; 3 | import { Proxy } from "./proxy"; 4 | 5 | export function createModuleProxy( 6 | proxy: Proxy, 7 | namespace: AmazonConnectNamespace, 8 | ): ModuleProxy { 9 | return { 10 | request: (command, data) => proxy.request(namespace, command, data), 11 | subscribe: (topic, handler) => 12 | proxy.subscribe({ ...topic, namespace }, handler), 13 | unsubscribe: (topic, handler) => 14 | proxy.unsubscribe({ ...topic, namespace }, handler), 15 | getProxyInfo: () => ({ 16 | connectionStatus: proxy.connectionStatus, 17 | proxyType: proxy.proxyType, 18 | }), 19 | onConnectionStatusChange: (h) => proxy.onConnectionStatusChange(h), 20 | offConnectionStatusChange: (h) => proxy.offConnectionStatusChange(h), 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /core/src/proxy/module-proxy.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ModuleSubscriptionTopic, 3 | SubscriptionHandler, 4 | SubscriptionHandlerData, 5 | } from "../messaging/subscription"; 6 | import { ConnectRequestData, ConnectResponseData } from "../request"; 7 | import { ProxyConnectionChangedHandler } from "./proxy-connection"; 8 | import { ProxyInfo } from "./proxy-info"; 9 | 10 | export interface ModuleProxy { 11 | request( 12 | command: string, 13 | data?: ConnectRequestData, 14 | ): Promise; 15 | subscribe( 16 | topic: ModuleSubscriptionTopic, 17 | handler: SubscriptionHandler, 18 | ): void; 19 | unsubscribe( 20 | topic: ModuleSubscriptionTopic, 21 | handler: SubscriptionHandler, 22 | ): void; 23 | getProxyInfo(): ProxyInfo; 24 | onConnectionStatusChange(handler: ProxyConnectionChangedHandler): void; 25 | offConnectionStatusChange(handler: ProxyConnectionChangedHandler): void; 26 | } 27 | -------------------------------------------------------------------------------- /core/src/proxy/proxy-connection/index.ts: -------------------------------------------------------------------------------- 1 | export { ProxyConnectionStatusManager } from "./proxy-connection-status-manager"; 2 | export type * from "./types"; 3 | -------------------------------------------------------------------------------- /core/src/proxy/proxy-connection/proxy-connection-status-manager.ts: -------------------------------------------------------------------------------- 1 | import { ConnectLogger, LogProvider } from "../../logging"; 2 | import { 3 | ProxyConnectionChangedHandler, 4 | ProxyConnectionEvent, 5 | ProxyConnectionStatus, 6 | } from "./types"; 7 | 8 | export class ProxyConnectionStatusManager { 9 | private readonly changeHandlers: Set; 10 | private readonly logger: ConnectLogger; 11 | private status: ProxyConnectionStatus; 12 | 13 | constructor(provider: LogProvider) { 14 | this.status = "notConnected"; 15 | this.changeHandlers = new Set(); 16 | this.logger = new ConnectLogger({ 17 | source: "core.proxy.connection-status-manager", 18 | provider, 19 | mixin: () => ({ status: this.status }), 20 | }); 21 | } 22 | 23 | getStatus(): ProxyConnectionStatus { 24 | return this.status; 25 | } 26 | 27 | update(evt: ProxyConnectionEvent): void { 28 | this.status = evt.status; 29 | this.logger.trace("Proxy Connection Status Changed", { 30 | status: evt.status, 31 | }); 32 | [...this.changeHandlers].forEach((handler) => { 33 | try { 34 | handler(evt); 35 | } catch (error) { 36 | this.logger.error( 37 | "An error occurred within a ProxyConnectionChangedHandler", 38 | { error }, 39 | ); 40 | } 41 | }); 42 | } 43 | 44 | onChange(handler: ProxyConnectionChangedHandler): void { 45 | this.changeHandlers.add(handler); 46 | } 47 | offChange(handler: ProxyConnectionChangedHandler): void { 48 | this.changeHandlers.delete(handler); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /core/src/proxy/proxy-connection/types.ts: -------------------------------------------------------------------------------- 1 | export type ProxyConnectionStatus = 2 | | "notConnected" 3 | | "connecting" 4 | | "initializing" 5 | | "ready" 6 | | "error" 7 | | "reset"; 8 | 9 | export type ProxyConnectionEvent = 10 | | ProxyConnecting 11 | | ProxyInitializing 12 | | ProxyReady 13 | | ProxyError 14 | | ProxyReset; 15 | 16 | export type ProxyConnecting = { 17 | status: "connecting"; 18 | }; 19 | 20 | export type ProxyInitializing = { 21 | status: "initializing"; 22 | }; 23 | 24 | export type ProxyReady = { 25 | status: "ready"; 26 | connectionId: string; 27 | }; 28 | 29 | export type ProxyError = { 30 | status: "error"; 31 | reason: string; 32 | details?: Record; 33 | }; 34 | 35 | export type ProxyReset = { 36 | status: "reset"; 37 | reason: string; 38 | }; 39 | 40 | export type ProxyConnectionChangedHandler = (evt: ProxyConnectionEvent) => void; 41 | -------------------------------------------------------------------------------- /core/src/proxy/proxy-factory.ts: -------------------------------------------------------------------------------- 1 | import { AmazonConnectProvider } from "../provider"; 2 | import { Proxy } from "./proxy"; 3 | 4 | export type ProxyFactory = ( 5 | provider: TProvider, 6 | ) => Proxy; 7 | -------------------------------------------------------------------------------- /core/src/proxy/proxy-info.ts: -------------------------------------------------------------------------------- 1 | import { ProxyConnectionStatus } from "./proxy-connection"; 2 | 3 | export type ProxyInfo = { 4 | connectionStatus: ProxyConnectionStatus; 5 | proxyType: string; 6 | }; 7 | -------------------------------------------------------------------------------- /core/src/proxy/proxy-subject-status.ts: -------------------------------------------------------------------------------- 1 | export type ProxySubjectStatus = 2 | | { initialized: false } 3 | | { 4 | initialized: true; 5 | startTime: Date; 6 | }; 7 | -------------------------------------------------------------------------------- /core/src/request/client-timeout-error.test.ts: -------------------------------------------------------------------------------- 1 | import { mock } from "jest-mock-extended"; 2 | 3 | import { RequestMessage, UpstreamMessageOrigin } from "../messaging"; 4 | import { 5 | clientTimeoutResponseErrorKey, 6 | formatClientTimeoutError, 7 | isClientTimeoutResponseError, 8 | } from "./client-timeout-error"; 9 | 10 | describe("formatClientTimeoutError", () => { 11 | test("should return a ClientTimeoutError", () => { 12 | const requestMessage: RequestMessage = { 13 | type: "request", 14 | namespace: "test-namespace", 15 | requestId: "id", 16 | command: "test-command", 17 | data: { foo: 1 }, 18 | messageOrigin: mock(), 19 | }; 20 | const timeout = 1000; 21 | 22 | const result = formatClientTimeoutError(requestMessage, timeout); 23 | 24 | expect(result.namespace).toEqual(requestMessage.namespace); 25 | expect(result.reason).toEqual("Client Timeout"); 26 | expect(result.errorKey).toEqual(clientTimeoutResponseErrorKey); 27 | expect(result.details.command).toEqual(requestMessage.command); 28 | expect(result.details.requestData).toEqual(requestMessage.data); 29 | expect(result.details.timeoutMs).toEqual(timeout); 30 | }); 31 | }); 32 | 33 | describe("isClientTimeoutResponseError", () => { 34 | test("should be false when value is not an object", () => { 35 | const result = isClientTimeoutResponseError("string"); 36 | 37 | expect(result).toBeFalsy(); 38 | }); 39 | 40 | test("should be false when error key does not match", () => { 41 | const otherError = { 42 | errorKey: "unknown", 43 | reason: "foo", 44 | namespace: "test-namespace", 45 | details: { 46 | command: "test-command", 47 | requestData: undefined, 48 | }, 49 | }; 50 | 51 | const result = isClientTimeoutResponseError(otherError); 52 | 53 | expect(result).toBeFalsy(); 54 | }); 55 | 56 | test("should be true when item is NoResultResponseError", () => { 57 | const error = formatClientTimeoutError( 58 | { 59 | type: "request", 60 | namespace: "test-namespace", 61 | requestId: "id", 62 | command: "test-command", 63 | data: undefined, 64 | messageOrigin: mock(), 65 | }, 66 | 5000, 67 | ); 68 | 69 | const result = isClientTimeoutResponseError(error); 70 | 71 | expect(result).toBeTruthy(); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /core/src/request/client-timeout-error.ts: -------------------------------------------------------------------------------- 1 | import { RequestMessage } from "../messaging"; 2 | 3 | type ClientTimeoutResponseErrorType = "clientTimeout"; 4 | export const clientTimeoutResponseErrorKey: ClientTimeoutResponseErrorType = 5 | "clientTimeout"; 6 | 7 | export function formatClientTimeoutError( 8 | request: RequestMessage, 9 | timeoutMs: number, 10 | ) { 11 | const { namespace, command, data: requestData } = request; 12 | 13 | return { 14 | namespace, 15 | reason: "Client Timeout", 16 | details: { 17 | command, 18 | requestData, 19 | timeoutMs, 20 | }, 21 | errorKey: clientTimeoutResponseErrorKey, 22 | }; 23 | } 24 | 25 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 26 | export function isClientTimeoutResponseError(err: any): boolean { 27 | return ( 28 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access 29 | typeof err === "object" && err.errorKey === clientTimeoutResponseErrorKey 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /core/src/request/handler-not-found-error.ts: -------------------------------------------------------------------------------- 1 | import { ConnectResponseError } from "./request-handlers"; 2 | 3 | export type HandlerNotFoundResponseErrorType = "handlerNotFound"; 4 | export const handlerNotFoundResponseErrorKey: HandlerNotFoundResponseErrorType = 5 | "handlerNotFound"; 6 | export type HandlerNotFoundResponseError = ConnectResponseError & { 7 | errorKey: HandlerNotFoundResponseErrorType; 8 | reason: "No handler for command"; 9 | }; 10 | -------------------------------------------------------------------------------- /core/src/request/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | formatClientTimeoutError, 3 | isClientTimeoutResponseError, 4 | } from "./client-timeout-error"; 5 | export type { 6 | HandlerNotFoundResponseError, 7 | HandlerNotFoundResponseErrorType, 8 | } from "./handler-not-found-error"; 9 | export { handlerNotFoundResponseErrorKey } from "./handler-not-found-error"; 10 | export type { 11 | NoResultResponseError, 12 | NoResultResponseErrorType, 13 | } from "./no-result-error"; 14 | export { 15 | isNoResultResponseError, 16 | noResultResponseErrorKey, 17 | } from "./no-result-error"; 18 | export type { 19 | ConnectRequest, 20 | ConnectRequestData, 21 | ConnectResponse, 22 | ConnectResponseData, 23 | ConnectResponseError, 24 | ConnectResponseSuccess, 25 | RequestId, 26 | } from "./request-handlers"; 27 | export { RequestManager } from "./request-manager"; 28 | export { createRequestMessage } from "./request-message-factory"; 29 | -------------------------------------------------------------------------------- /core/src/request/no-result-error.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | isNoResultResponseError, 3 | NoResultResponseError, 4 | } from "./no-result-error"; 5 | import { ConnectResponseError } from "./request-handlers"; 6 | 7 | describe("isNoResultResponseError", () => { 8 | test("should be false when value is not an object", () => { 9 | const result = isNoResultResponseError("string"); 10 | 11 | expect(result).toBeFalsy(); 12 | }); 13 | 14 | test("should be false when error key does not match", () => { 15 | const otherError: ConnectResponseError = { 16 | isError: true, 17 | errorKey: "unknown", 18 | reason: "foo", 19 | namespace: "test-namespace", 20 | requestId: "id", 21 | details: { 22 | command: "test-command", 23 | requestData: undefined, 24 | }, 25 | }; 26 | 27 | const result = isNoResultResponseError(otherError); 28 | 29 | expect(result).toBeFalsy(); 30 | }); 31 | 32 | test("should be true when item is NoResultResponseError", () => { 33 | const error: NoResultResponseError = { 34 | isError: true, 35 | errorKey: "noResult", 36 | reason: "No Result Found", 37 | namespace: "test-namespace", 38 | requestId: "id", 39 | details: { 40 | command: "test-command", 41 | requestData: undefined, 42 | }, 43 | }; 44 | 45 | const result = isNoResultResponseError(error); 46 | 47 | expect(result).toBeTruthy(); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /core/src/request/no-result-error.ts: -------------------------------------------------------------------------------- 1 | import { ConnectResponseError } from "./request-handlers"; 2 | 3 | export type NoResultResponseErrorType = "noResult"; 4 | export const noResultResponseErrorKey: NoResultResponseErrorType = "noResult"; 5 | export type NoResultResponseError = ConnectResponseError & { 6 | errorKey: NoResultResponseErrorType; 7 | reason: "No Result Found"; 8 | }; 9 | 10 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 11 | export function isNoResultResponseError(err: any): boolean { 12 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access 13 | return typeof err === "object" && err.errorKey === noResultResponseErrorKey; 14 | } 15 | -------------------------------------------------------------------------------- /core/src/request/request-handler-factory.ts: -------------------------------------------------------------------------------- 1 | import { ConnectError } from "../error"; 2 | import { RequestMessage, ResponseMessage } from "../messaging"; 3 | import { formatClientTimeoutError } from "./client-timeout-error"; 4 | import { ConnectResponseData } from "./request-handlers"; 5 | 6 | export type ResponseHandler = (msg: ResponseMessage) => void; 7 | 8 | const DEFAULT_TIMEOUT_MS = 30 * 1000; 9 | 10 | export function createRequestHandler( 11 | request: RequestMessage, 12 | onStart: (handler: ResponseHandler) => void, 13 | onTimeout: (details: { timeoutMs: number; request: RequestMessage }) => void, 14 | timeoutMs?: number, 15 | ): Promise { 16 | const adjustedTimeoutMs = Math.max(1, timeoutMs ?? DEFAULT_TIMEOUT_MS); 17 | 18 | return new Promise((resolve, reject) => { 19 | let isTimedOut = false; 20 | const timeout = setTimeout(() => { 21 | onTimeout({ timeoutMs: adjustedTimeoutMs, request }); 22 | // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors 23 | reject(formatClientTimeoutError(request, adjustedTimeoutMs)); 24 | isTimedOut = true; 25 | }, adjustedTimeoutMs); 26 | 27 | const handler = (msg: ResponseMessage) => { 28 | clearTimeout(timeout); 29 | if (!isTimedOut) { 30 | if (msg.isError) { 31 | reject(new ConnectError(msg)); 32 | } else { 33 | resolve(msg.data as TResponse); 34 | } 35 | } 36 | }; 37 | 38 | onStart(handler); 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /core/src/request/request-handlers.ts: -------------------------------------------------------------------------------- 1 | import { AmazonConnectNamespace } from "../amazon-connect-namespace"; 2 | 3 | export type ConnectRequestData = object | void; 4 | 5 | export type RequestId = string | number; 6 | 7 | export type ConnectRequest = 8 | { 9 | namespace: AmazonConnectNamespace; 10 | command: string; 11 | requestId: RequestId; 12 | data: T; 13 | }; 14 | 15 | type BaseConnectResponse = { 16 | namespace: AmazonConnectNamespace; 17 | requestId: RequestId; 18 | }; 19 | 20 | export type ConnectResponseData = object | void; 21 | 22 | export type ConnectResponseSuccess< 23 | T extends ConnectResponseData = ConnectResponseData, 24 | > = BaseConnectResponse & { 25 | isError: false; 26 | data: T; 27 | }; 28 | 29 | export type ConnectResponseError< 30 | T extends ConnectRequestData = ConnectRequestData, 31 | > = BaseConnectResponse & { 32 | isError: true; 33 | errorKey: string; 34 | reason: string; 35 | details: { command: string; requestData: T } & Record; 36 | }; 37 | 38 | export type ConnectResponse = ConnectResponseSuccess | ConnectResponseError; 39 | -------------------------------------------------------------------------------- /core/src/request/request-manager.ts: -------------------------------------------------------------------------------- 1 | import { ConnectLogger, LogProvider } from "../logging"; 2 | import { RequestMessage, ResponseMessage } from "../messaging"; 3 | import { 4 | createRequestHandler, 5 | ResponseHandler, 6 | } from "./request-handler-factory"; 7 | import { ConnectResponseData, RequestId } from "./request-handlers"; 8 | 9 | export class RequestManager { 10 | private readonly requestMap: Map; 11 | private readonly logger: ConnectLogger; 12 | 13 | constructor(provider: LogProvider) { 14 | this.requestMap = new Map(); 15 | this.logger = new ConnectLogger({ 16 | provider, 17 | source: "core.requestManager", 18 | }); 19 | } 20 | 21 | processRequest( 22 | request: RequestMessage, 23 | ): Promise { 24 | const { requestId } = request; 25 | 26 | return createRequestHandler( 27 | request, 28 | (handler) => this.requestMap.set(requestId, handler), 29 | ({ request, timeoutMs }) => this.handleTimeout(request, timeoutMs), 30 | ); 31 | } 32 | 33 | processResponse(response: ResponseMessage): void { 34 | const { requestId } = response; 35 | 36 | const handler = this.requestMap.get(requestId); 37 | 38 | if (!handler) { 39 | // The proxy is implemented such that this should never happen 40 | this.logger.error("Returned a response message with no handler", { 41 | message: response, 42 | }); 43 | return; 44 | } 45 | 46 | handler(response); 47 | this.requestMap.delete(requestId); 48 | } 49 | 50 | private handleTimeout(request: RequestMessage, timeoutMs: number): void { 51 | const { requestId, namespace, command } = request; 52 | this.requestMap.delete(requestId); 53 | 54 | this.logger.error("Client request timeout", { 55 | requestId, 56 | namespace, 57 | command, 58 | timeoutMs, 59 | }); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /core/src/request/request-message-factory.test.ts: -------------------------------------------------------------------------------- 1 | import { mocked } from "jest-mock"; 2 | import { mock } from "jest-mock-extended"; 3 | 4 | import { UpstreamMessageOrigin } from "../messaging"; 5 | import { generateUUID } from "../utility"; 6 | import { createRequestMessage } from "./request-message-factory"; 7 | jest.mock("../utility/id-generator"); 8 | 9 | describe("createRequestMessage", () => { 10 | test("should create message with requestId", () => { 11 | const namespace = "namespace-1"; 12 | const command = "command-1"; 13 | const data = { foo: 1 }; 14 | const messageOrigin = mock(); 15 | const requestId = "abc"; 16 | mocked(generateUUID).mockReturnValueOnce(requestId); 17 | 18 | const result = createRequestMessage( 19 | namespace, 20 | command, 21 | data, 22 | messageOrigin, 23 | ); 24 | 25 | expect(result.namespace).toEqual(namespace); 26 | expect(result.command).toEqual(command); 27 | expect(result.data).toEqual(data); 28 | expect(result.messageOrigin).toEqual(messageOrigin); 29 | expect(result.requestId).toEqual(requestId); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /core/src/request/request-message-factory.ts: -------------------------------------------------------------------------------- 1 | import { AmazonConnectNamespace } from "../amazon-connect-namespace"; 2 | import { RequestMessage, UpstreamMessageOrigin } from "../messaging"; 3 | import { generateUUID } from "../utility"; 4 | import { ConnectRequestData } from "./request-handlers"; 5 | 6 | export function createRequestMessage( 7 | namespace: AmazonConnectNamespace, 8 | command: string, 9 | data: ConnectRequestData | undefined, 10 | messageOrigin: UpstreamMessageOrigin, 11 | ): RequestMessage { 12 | const requestId = generateUUID(); 13 | return { 14 | type: "request", 15 | namespace, 16 | command, 17 | requestId, 18 | data, 19 | messageOrigin, 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /core/src/sdk-version.ts: -------------------------------------------------------------------------------- 1 | // Do not edit this file manually 2 | export const sdkVersion = "1.0.6"; 3 | -------------------------------------------------------------------------------- /core/src/utility/emitter/async-event-emitter.ts: -------------------------------------------------------------------------------- 1 | import { EmitterBase } from "./emitter-base"; 2 | 3 | export class AsyncEventEmitter extends EmitterBase< 4 | (evt: TEvent) => Promise 5 | > { 6 | async emit(parameter: string, event: TEvent): Promise { 7 | const handlers = this.getHandlers(parameter); 8 | await Promise.allSettled( 9 | handlers.map(async (handler) => { 10 | try { 11 | await handler(event); 12 | } catch (error) { 13 | this.logger.error("An error occurred when invoking event handler", { 14 | error, 15 | parameter, 16 | }); 17 | } 18 | }), 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /core/src/utility/emitter/emitter-base.ts: -------------------------------------------------------------------------------- 1 | import { ConnectLogger, LogProvider } from "../../logging"; 2 | 3 | export type EmitterParams = { 4 | provider: LogProvider; 5 | loggerKey?: string; 6 | }; 7 | 8 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 9 | type EmitterHandler = (...args: any[]) => Promise | void; 10 | 11 | export abstract class EmitterBase { 12 | private readonly events: Map>; 13 | protected readonly logger: ConnectLogger; 14 | 15 | constructor({ provider, loggerKey }: EmitterParams) { 16 | this.events = new Map(); 17 | 18 | this.logger = new ConnectLogger({ 19 | provider, 20 | source: "emitter", 21 | mixin: () => ({ 22 | emitterLoggerKey: loggerKey, 23 | }), 24 | }); 25 | } 26 | 27 | on(parameter: string, handler: THandler): void { 28 | const set = this.events.get(parameter); 29 | if (set) set.add(handler); 30 | else this.events.set(parameter, new Set([handler])); 31 | } 32 | 33 | off(parameter: string, handler: THandler): void { 34 | const set = this.events.get(parameter); 35 | if (set) { 36 | set.delete(handler); 37 | if (set.size < 1) this.events.delete(parameter); 38 | } 39 | } 40 | 41 | protected getHandlers(parameter: string): THandler[] { 42 | return Array.from(this.events.get(parameter) ?? []); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /core/src/utility/emitter/emitter.ts: -------------------------------------------------------------------------------- 1 | import { EmitterBase } from "./emitter-base"; 2 | 3 | export class Emitter extends EmitterBase<() => void> { 4 | emit(parameter: string): void { 5 | for (const handler of this.getHandlers(parameter)) { 6 | try { 7 | handler(); 8 | } catch (error) { 9 | this.logger.error("An error occurred when invoking handler", { 10 | error, 11 | parameter, 12 | }); 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /core/src/utility/emitter/event-emitter.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/unbound-method */ 2 | import { MockedClass, MockedObject } from "jest-mock"; 3 | import { mock } from "jest-mock-extended"; 4 | 5 | import { ConnectLogger } from "../../logging"; 6 | import { AmazonConnectProvider } from "../../provider"; 7 | import { EventEmitter } from "./event-emitter"; 8 | 9 | jest.mock("../../logging/connect-logger"); 10 | 11 | const LoggerMock = ConnectLogger as MockedClass; 12 | 13 | const provider = mock(); 14 | let loggerMock: MockedObject; 15 | const testKey = "test-key"; 16 | 17 | type TestEvent = { foo: string }; 18 | const testEvent: TestEvent = { foo: "1" }; 19 | 20 | beforeEach(jest.resetAllMocks); 21 | 22 | describe("EventEmitter", () => { 23 | let sut: EventEmitter; 24 | const p1 = "p1"; 25 | 26 | beforeEach(() => { 27 | sut = new EventEmitter({ provider, loggerKey: testKey }); 28 | 29 | loggerMock = LoggerMock.mock.instances[0]; 30 | }); 31 | 32 | describe("emit", () => { 33 | test("should invoke handler when emitted", () => { 34 | const handler = jest.fn(); 35 | sut.on(p1, handler); 36 | 37 | sut.emit(p1, testEvent); 38 | 39 | expect(handler).toBeCalledTimes(1); 40 | expect(handler).toHaveBeenCalledWith(testEvent); 41 | }); 42 | 43 | test("should invoke handler added multiple times once", () => { 44 | const handler = jest.fn(); 45 | sut.on(p1, handler); 46 | sut.on(p1, handler); 47 | 48 | sut.emit(p1, testEvent); 49 | 50 | expect(handler).toBeCalledTimes(1); 51 | expect(handler).toHaveBeenCalledWith(testEvent); 52 | }); 53 | 54 | test("should invoke multiple handlers", () => { 55 | const handler1 = jest.fn(); 56 | const handler2 = jest.fn(); 57 | sut.on(p1, handler1); 58 | sut.on(p1, handler2); 59 | 60 | sut.emit(p1, testEvent); 61 | 62 | expect(handler1).toBeCalledTimes(1); 63 | expect(handler1).toHaveBeenCalledWith(testEvent); 64 | expect(handler2).toBeCalledTimes(1); 65 | expect(handler2).toHaveBeenCalledWith(testEvent); 66 | }); 67 | 68 | test("should invoke additional handlers after one throws error", () => { 69 | const handler2Error = new Error("handler 2"); 70 | const handler4Error = new Error("handler 4"); 71 | 72 | const handler1 = jest.fn(); 73 | const handler2 = jest.fn().mockImplementationOnce(() => { 74 | throw handler2Error; 75 | }); 76 | const handler3 = jest.fn(); 77 | const handler4 = jest.fn().mockImplementationOnce(() => { 78 | throw handler4Error; 79 | }); 80 | sut.on(p1, handler1); 81 | sut.on(p1, handler2); 82 | sut.on(p1, handler3); 83 | sut.on(p1, handler4); 84 | 85 | sut.emit(p1, testEvent); 86 | 87 | expect(loggerMock.error).toBeCalledTimes(2); 88 | expect(loggerMock.error).toBeCalledWith(expect.any(String), { 89 | error: handler2Error, 90 | parameter: p1, 91 | }); 92 | expect(loggerMock.error).toBeCalledWith(expect.any(String), { 93 | error: handler4Error, 94 | parameter: p1, 95 | }); 96 | expect(handler1).toBeCalledTimes(1); 97 | expect(handler2).toBeCalledTimes(1); 98 | expect(handler3).toBeCalledTimes(1); 99 | expect(handler4).toBeCalledTimes(1); 100 | }); 101 | }); 102 | }); 103 | -------------------------------------------------------------------------------- /core/src/utility/emitter/event-emitter.ts: -------------------------------------------------------------------------------- 1 | import { EmitterBase } from "./emitter-base"; 2 | 3 | export class EventEmitter extends EmitterBase<(evt: TEvent) => void> { 4 | emit(parameter: string, event: TEvent): void { 5 | const handlers = this.getHandlers(parameter); 6 | for (const handler of handlers) { 7 | try { 8 | handler(event); 9 | } catch (error) { 10 | this.logger.error("An error occurred when invoking event handler", { 11 | error, 12 | parameter, 13 | }); 14 | } 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /core/src/utility/emitter/index.ts: -------------------------------------------------------------------------------- 1 | export { AsyncEventEmitter } from "./async-event-emitter"; 2 | export { Emitter } from "./emitter"; 3 | export type { EmitterParams } from "./emitter-base"; 4 | export { EventEmitter } from "./event-emitter"; 5 | -------------------------------------------------------------------------------- /core/src/utility/id-generator.test.ts: -------------------------------------------------------------------------------- 1 | import { webcrypto } from "crypto"; 2 | 3 | import { generateStringId, generateUUID } from "./id-generator"; 4 | 5 | let originalCrypto: Crypto; 6 | 7 | beforeAll(() => { 8 | originalCrypto = { ...global.crypto }; 9 | }); 10 | 11 | describe("On JS runtime that supports crypto.randomUUID", () => { 12 | beforeAll(() => { 13 | global.crypto = { 14 | ...originalCrypto, 15 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-explicit-any 16 | getRandomValues: (arr) => webcrypto.getRandomValues(arr as any), 17 | randomUUID: () => webcrypto.randomUUID(), 18 | }; 19 | }); 20 | 21 | describe("generateStringId", () => { 22 | test("should have length of 1", () => { 23 | const result = generateStringId(1); 24 | 25 | expect(result).toHaveLength(1); 26 | }); 27 | 28 | test("should have length of 9", () => { 29 | const result = generateStringId(9); 30 | 31 | expect(result).toHaveLength(9); 32 | }); 33 | 34 | test("should have length of 10", () => { 35 | const result = generateStringId(10); 36 | 37 | expect(result).toHaveLength(10); 38 | }); 39 | 40 | test("should have length of 53", () => { 41 | const result = generateStringId(53); 42 | 43 | expect(result).toHaveLength(53); 44 | }); 45 | 46 | test("should only contain hex characters", () => { 47 | const result = generateStringId(30); 48 | const hexValues = [..."0123456789abcdef"]; 49 | 50 | expect([...result].every((r) => hexValues.includes(r))).toBeTruthy(); 51 | }); 52 | 53 | test("should not contain the same value in every position", () => { 54 | const result = generateStringId(30); 55 | const counts = [...result].reduce( 56 | (a, e) => { 57 | a[e] = a[e] ? a[e] + 1 : 1; 58 | return a; 59 | }, 60 | {} as Record, 61 | ); 62 | 63 | expect(Object.values(counts).includes(result.length)).toBeFalsy(); 64 | }); 65 | }); 66 | 67 | describe("generateUUID", () => { 68 | test("should match UUID regex", () => { 69 | const regex = 70 | /^[0-9A-F]{8}-[0-9A-F]{4}-[4][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i; 71 | const result = generateUUID(); 72 | expect(result).toMatch(regex); 73 | }); 74 | }); 75 | }); 76 | 77 | describe("On JS runtime that doesn't support crypto.randomUUID", () => { 78 | beforeAll(() => { 79 | global.crypto = { 80 | ...originalCrypto, 81 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-explicit-any 82 | getRandomValues: (arr) => webcrypto.getRandomValues(arr as any), 83 | }; 84 | }); 85 | describe("generateUUID", () => { 86 | test("should match UUID regex", () => { 87 | const regex = 88 | /^[0-9A-F]{8}-[0-9A-F]{4}-[4][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i; 89 | const result = generateUUID(); 90 | expect(result).toMatch(regex); 91 | }); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /core/src/utility/id-generator.ts: -------------------------------------------------------------------------------- 1 | export function generateStringId(length: number): string { 2 | const a = new Uint8Array(Math.ceil(length / 2)); 3 | crypto.getRandomValues(a); 4 | return Array.from(a, (d) => d.toString(16).padStart(2, "0")) 5 | .join("") 6 | .substring(0, length); 7 | } 8 | 9 | export function generateUUID(): string { 10 | if ("randomUUID" in crypto) { 11 | return crypto.randomUUID(); 12 | } else { 13 | return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, (c) => { 14 | const d = parseInt(c); 15 | return ( 16 | d ^ 17 | (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (d / 4))) 18 | ).toString(16); 19 | }); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /core/src/utility/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./emitter"; 2 | export { generateStringId, generateUUID } from "./id-generator"; 3 | export { getOriginAndPath } from "./location-helpers"; 4 | export { SubscriptionHandlerRelay } from "./subscription-handler-relay"; 5 | export type { 6 | TimeoutTrackerCancelledEvent, 7 | TimeoutTrackerCancelledHandler, 8 | TimeoutTrackerStatus, 9 | } from "./timeout-tracker"; 10 | export { TimeoutTracker } from "./timeout-tracker"; 11 | -------------------------------------------------------------------------------- /core/src/utility/location-helper.test.ts: -------------------------------------------------------------------------------- 1 | import { mock } from "jest-mock-extended"; 2 | 3 | import { getOriginAndPath } from "./location-helpers"; 4 | 5 | let originalDocument: Document; 6 | 7 | beforeAll(() => { 8 | originalDocument = global.document; 9 | }); 10 | 11 | afterEach(() => { 12 | global.document = originalDocument; 13 | }); 14 | 15 | describe("getOriginAndPath", () => { 16 | describe("when document.location is defined", () => { 17 | test("should return origin and path", () => { 18 | const origin = "https://test.com"; 19 | const path = "/path"; 20 | global.document = { 21 | ...originalDocument, 22 | location: mock({ origin, pathname: path }), 23 | }; 24 | 25 | const result = getOriginAndPath(); 26 | 27 | expect(result.origin).toEqual(origin); 28 | expect(result.path).toEqual(path); 29 | }); 30 | }); 31 | 32 | describe("when document is not defined", () => { 33 | test("should return origin and path as unknown", () => { 34 | (global as { document?: unknown }).document = undefined; 35 | 36 | const result = getOriginAndPath(); 37 | 38 | expect(result.origin).toEqual("unknown"); 39 | expect(result.path).toEqual("unknown"); 40 | }); 41 | }); 42 | 43 | describe("when document.location is not defined", () => { 44 | test("should return origin and path as unknown", () => { 45 | global.document = { 46 | ...originalDocument, 47 | location: undefined as unknown as Location, 48 | }; 49 | 50 | const result = getOriginAndPath(); 51 | 52 | expect(result.origin).toEqual("unknown"); 53 | expect(result.path).toEqual("unknown"); 54 | }); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /core/src/utility/location-helpers.ts: -------------------------------------------------------------------------------- 1 | export function getOriginAndPath(): { origin: string; path: string } { 2 | return { 3 | origin: document?.location?.origin ?? "unknown", 4 | path: document?.location?.pathname ?? "unknown", 5 | }; 6 | } 7 | -------------------------------------------------------------------------------- /core/src/utility/timeout-tracker.ts: -------------------------------------------------------------------------------- 1 | import { ConnectLogger } from "../logging"; 2 | 3 | export type TimeoutTrackerStatus = "running" | "completed" | "cancelled"; 4 | 5 | export type TimeoutTrackerCancelledEvent = { 6 | timeoutMs: number; 7 | }; 8 | 9 | export type TimeoutTrackerCancelledHandler = ( 10 | evt: TimeoutTrackerCancelledEvent, 11 | ) => void; 12 | 13 | export class TimeoutTracker { 14 | public readonly timeoutMs; 15 | private readonly onCancelled: TimeoutTrackerCancelledHandler; 16 | private status: TimeoutTrackerStatus; 17 | private readonly logger: ConnectLogger; 18 | private timeout: NodeJS.Timeout; 19 | 20 | constructor(onCancelled: TimeoutTrackerCancelledHandler, timeoutMs: number) { 21 | this.timeoutMs = timeoutMs; 22 | this.onCancelled = onCancelled; 23 | this.timeout = setTimeout(() => this.handleCancel(), this.timeoutMs); 24 | this.status = "running"; 25 | this.logger = new ConnectLogger({ 26 | source: "core.utility.timeout-tracker", 27 | mixin: () => ({ 28 | timeoutMs: this.timeoutMs, 29 | timeoutTrackerStatus: this.status, 30 | }), 31 | }); 32 | } 33 | 34 | static start( 35 | onCancelled: TimeoutTrackerCancelledHandler, 36 | ms: number, 37 | ): TimeoutTracker { 38 | return new TimeoutTracker(onCancelled, ms); 39 | } 40 | 41 | complete(): boolean { 42 | switch (this.status) { 43 | case "running": 44 | return this.handleComplete(); 45 | case "completed": 46 | this.logger.debug("TimeoutTracker already marked complete. No action."); 47 | return true; 48 | case "cancelled": 49 | this.logger.info( 50 | "Attempted to complete a TimeoutTracker that has already been cancelled", 51 | ); 52 | return false; 53 | } 54 | } 55 | 56 | isCancelled(): boolean { 57 | return this.status === "cancelled"; 58 | } 59 | 60 | getStatus(): TimeoutTrackerStatus { 61 | return this.status; 62 | } 63 | 64 | private handleCancel(): void { 65 | switch (this.status) { 66 | case "running": 67 | this.status = "cancelled"; 68 | this.logger.info( 69 | "TimeoutTracker has timed out. Invoking onCancelled Handler", 70 | ); 71 | this.invokeOnCancelled(); 72 | break; 73 | case "completed": 74 | this.logger.debug( 75 | "Cancel operation for TimerTracker invoked after already completed. No action.", 76 | ); 77 | break; 78 | default: 79 | throw new Error( 80 | "Cancel operation in TimerTracker called during an unexpected time.", 81 | ); 82 | } 83 | } 84 | 85 | private handleComplete(): boolean { 86 | this.status = "completed"; 87 | clearTimeout(this.timeout); 88 | return true; 89 | } 90 | 91 | private invokeOnCancelled() { 92 | try { 93 | this.onCancelled({ timeoutMs: this.timeoutMs }); 94 | } catch (error) { 95 | this.logger.error( 96 | "Error when attempting to invoke TimeoutTrackerCancelledHandler", 97 | { error }, 98 | ); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /core/tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "CommonJS", 5 | }, 6 | "exclude": ["./src/**/*.test.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /core/tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib-esm", 5 | }, 6 | "exclude": ["./src/**/*.test.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib/", 5 | }, 6 | "include": ["./src/**/*"] 7 | } 8 | -------------------------------------------------------------------------------- /email/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ["@babel/preset-env", { targets: { node: "current" } }], 4 | "@babel/preset-typescript", 5 | ], 6 | }; 7 | -------------------------------------------------------------------------------- /email/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@amazon-connect/email", 3 | "version": "1.0.6", 4 | "description": "Email functionality of the Amazon Connect SDK", 5 | "publishConfig": { 6 | "access": "public" 7 | }, 8 | "scripts": { 9 | "build:esm": "tsc -p tsconfig.esm.json", 10 | "build:cjs": "tsc -p tsconfig.cjs.json", 11 | "build": "npm run build:esm && npm run build:cjs", 12 | "check-types": "tsc --noEmit", 13 | "clean": "rm -rf ./lib ./lib-esm ./build", 14 | "prepack": "npm run build", 15 | "test": "jest --coverage" 16 | }, 17 | "license": "Apache-2.0", 18 | "repository": { 19 | "type": "git", 20 | "url": "ssh://git.amazon.com/pkg/AmazonConnectSDK" 21 | }, 22 | "main": "lib/index.js", 23 | "module": "lib-esm/index.js", 24 | "types": "lib/index.d.ts", 25 | "files": [ 26 | "lib/**/*", 27 | "lib-esm/**/*" 28 | ], 29 | "dependencies": { 30 | "@amazon-connect/core": "1.0.6" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /email/src/email-namespace.ts: -------------------------------------------------------------------------------- 1 | export const emailNamespace = "aws.connect.email"; 2 | -------------------------------------------------------------------------------- /email/src/error/error-helpers.ts: -------------------------------------------------------------------------------- 1 | import { isConnectError } from "@amazon-connect/core"; 2 | 3 | import { emailNamespace } from "../email-namespace"; 4 | 5 | /** 6 | * Utility function to check if the error is an `OutboundEmailAddressNotConfiguredError`. 7 | * 8 | * This error is surfaced when the routing profile's default outbound queue does not have 9 | * a default outbound email address and the request to `sendEmail` does not include a `from` 10 | * address. This will only be thrown when `sendEmail` is called. 11 | * 12 | * @param {unknown} e the error 13 | * @returns {boolean} true if the error is an `OutboundEmailAddressNotConfiguredError`, false otherwise 14 | */ 15 | export function isOutboundEmailAddressNotConfiguredError(e: unknown): boolean { 16 | return ( 17 | isConnectError(e) && 18 | e.namespace === emailNamespace && 19 | e.errorKey === "OutboundEmailAddressNotConfiguredException" 20 | ); 21 | } 22 | 23 | /** 24 | * Utility function to check if the error is an `EmailBodySizeExceededError`. 25 | * 26 | * This error is surfaced when the size of the email body exceeds the limit. This will 27 | * be thrown when the `sendEmail` method is called. 28 | * 29 | * @param {unknown} e the error 30 | * @returns {boolean} true if the error is an `EmailBodySizeExceededError`, false otherwise 31 | */ 32 | export function isEmailBodySizeExceededError(e: unknown): boolean { 33 | return ( 34 | isConnectError(e) && 35 | e.namespace === emailNamespace && 36 | e.errorKey === "EmailBodySizeExceededException" 37 | ); 38 | } 39 | /** 40 | * Utility function to check if the error is a `TotalEmailSizeExceededError`. 41 | * 42 | * This error is surfaced when the total size of the email (email body and all attachments) 43 | * exceeds the limit. This will be thrown when the `sendEmail` method is called. 44 | * 45 | * @param {unknown} e the error 46 | * @returns {boolean} true if the error is a `TotalEmailSizeExceededError`, false otherwise 47 | */ 48 | export function isTotalEmailSizeExceededError(e: unknown): boolean { 49 | return ( 50 | isConnectError(e) && 51 | e.namespace === emailNamespace && 52 | e.errorKey === "TotalEmailSizeExceededException" 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /email/src/error/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | isEmailBodySizeExceededError, 3 | isOutboundEmailAddressNotConfiguredError, 4 | isTotalEmailSizeExceededError, 5 | } from "./error-helpers"; 6 | -------------------------------------------------------------------------------- /email/src/index.ts: -------------------------------------------------------------------------------- 1 | export { EmailClient } from "./email-client"; 2 | export { emailNamespace } from "./email-namespace"; 3 | export * from "./error"; 4 | export { EmailRoute } from "./routes"; 5 | export { EmailContactEvents } from "./topic-keys"; 6 | export * from "./types"; 7 | -------------------------------------------------------------------------------- /email/src/routes.ts: -------------------------------------------------------------------------------- 1 | export enum EmailRoute { 2 | getEmailData = "get-email-data", 3 | createDraftEmail = "create-draft-email", 4 | sendEmail = "send-email", 5 | getEmailThread = "get-email-thread", 6 | } 7 | -------------------------------------------------------------------------------- /email/src/topic-keys.ts: -------------------------------------------------------------------------------- 1 | export enum EmailContactEvents { 2 | /** 3 | * The agent has accepted an inbound email contact. 4 | * 5 | * @see ContactLifecycleTopicKey.Connected 6 | */ 7 | InboundContactConnected = "inbound-contact-connected", 8 | 9 | /** 10 | * An outbound email contact has been assigned to the agent. 11 | * 12 | * @see ContactLifecycleTopicKey.Connected 13 | */ 14 | OutboundContactConnected = "outbound-contact-connected", 15 | } 16 | -------------------------------------------------------------------------------- /email/tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "CommonJS", 5 | }, 6 | "exclude": ["./src/**/*.test.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /email/tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib-esm", 5 | }, 6 | "exclude": ["./src/**/*.test.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /email/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib/", 5 | }, 6 | "include": ["./src/**/*"] 7 | } 8 | -------------------------------------------------------------------------------- /file/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ["@babel/preset-env", { targets: { node: "current" } }], 4 | "@babel/preset-typescript", 5 | ], 6 | }; 7 | -------------------------------------------------------------------------------- /file/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@amazon-connect/file", 3 | "version": "1.0.6", 4 | "description": "Files functionality of the Amazon Connect SDK", 5 | "publishConfig": { 6 | "access": "public" 7 | }, 8 | "scripts": { 9 | "build:esm": "tsc -p tsconfig.esm.json", 10 | "build:cjs": "tsc -p tsconfig.cjs.json", 11 | "build": "npm run build:esm && npm run build:cjs", 12 | "check-types": "tsc --noEmit", 13 | "clean": "rm -rf ./lib ./lib-esm ./build", 14 | "prepack": "npm run build", 15 | "test": "jest --coverage" 16 | }, 17 | "license": "Apache-2.0", 18 | "repository": { 19 | "type": "git", 20 | "url": "ssh://git.amazon.com/pkg/AmazonConnectSDK" 21 | }, 22 | "main": "lib/index.js", 23 | "module": "lib-esm/index.js", 24 | "types": "lib/index.d.ts", 25 | "files": [ 26 | "lib/**/*", 27 | "lib-esm/**/*" 28 | ], 29 | "dependencies": { 30 | "@amazon-connect/core": "1.0.6" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /file/src/error/error-helpers.ts: -------------------------------------------------------------------------------- 1 | import { isConnectError } from "@amazon-connect/core"; 2 | 3 | import { fileNamespace } from "../file-namespace"; 4 | 5 | /** 6 | * Utility function to check if the error is an `InvalidFileNameError`. 7 | * 8 | * This will be thrown when `startAttachedFileUpload` is called 9 | * 10 | * @param {unknown} e the error 11 | * @returns {boolean} true if the error is an `InvalidFileNameError`, false otherwise 12 | */ 13 | export function isInvalidFileNameError(e: unknown): boolean { 14 | return ( 15 | isConnectError(e) && 16 | e.namespace === fileNamespace && 17 | e.errorKey === "InvalidFileNameException" 18 | ); 19 | } 20 | 21 | /** 22 | * Utility function to check if the error is an `InvalidFileTypeError`. 23 | * 24 | * This will be thrown when `startAttachedFileUpload` is called 25 | * 26 | * @param {unknown} e the error 27 | * @returns {boolean} true if the error is an `InvalidFileTypeError`, false otherwise 28 | */ 29 | export function isInvalidFileTypeError(e: unknown): boolean { 30 | return ( 31 | isConnectError(e) && 32 | e.namespace === fileNamespace && 33 | e.errorKey === "InvalidFileTypeException" 34 | ); 35 | } 36 | 37 | /** 38 | * Utility function to check if the error is an `InvalidFileSizeError`. 39 | * 40 | * This will be thrown when `startAttachedFileUpload` is called 41 | * 42 | * @param {unknown} e the error 43 | * @returns {boolean} true if the error is an `InvalidFileSizeError`, false otherwise 44 | */ 45 | export function isInvalidFileSizeError(e: unknown): boolean { 46 | return ( 47 | isConnectError(e) && 48 | e.namespace === fileNamespace && 49 | e.errorKey === "InvalidFileSizeException" 50 | ); 51 | } 52 | 53 | /** 54 | * Utility function to check if the error is a `TotalFileSizeExceededError`. 55 | * 56 | * This will be thrown when `startAttachedFileUpload` is called 57 | * 58 | * @param {unknown} e the error 59 | * @returns {boolean} true if the error is a `TotalFileSizeExceededError`, false otherwise 60 | */ 61 | export function isTotalFileSizeExceededError(e: unknown): boolean { 62 | return ( 63 | isConnectError(e) && 64 | e.namespace === fileNamespace && 65 | e.errorKey === "TotalFileSizeExceededException" 66 | ); 67 | } 68 | 69 | /** 70 | * Utility function to check if the error is a `TotalFileCountExceededError`. 71 | * 72 | * This will be thrown when `startAttachedFileUpload` is called 73 | * 74 | * @param {unknown} e the error 75 | * @returns {boolean} true if the error is a `TotalFileCountExceededError`, false otherwise 76 | */ 77 | export function isTotalFileCountExceededError(e: unknown): boolean { 78 | return ( 79 | isConnectError(e) && 80 | e.namespace === fileNamespace && 81 | e.errorKey === "TotalFileCountExceededException" 82 | ); 83 | } 84 | -------------------------------------------------------------------------------- /file/src/error/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | isInvalidFileNameError, 3 | isInvalidFileSizeError, 4 | isInvalidFileTypeError, 5 | isTotalFileCountExceededError, 6 | isTotalFileSizeExceededError, 7 | } from "./error-helpers"; 8 | -------------------------------------------------------------------------------- /file/src/file-namespace.ts: -------------------------------------------------------------------------------- 1 | export const fileNamespace = "aws.connect.file"; 2 | -------------------------------------------------------------------------------- /file/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./error"; 2 | export { FileClient } from "./file-client"; 3 | export { fileNamespace } from "./file-namespace"; 4 | export { FileRoute } from "./routes"; 5 | export * from "./types"; 6 | -------------------------------------------------------------------------------- /file/src/routes.ts: -------------------------------------------------------------------------------- 1 | export enum FileRoute { 2 | batchGetAttachedFileMetadata = "batch-get-attached-file-metadata", 3 | startAttachedFileUpload = "start-attached-file-upload", 4 | completeAttachedFileUpload = "complete-attached-file-upload", 5 | getAttachedFileUrl = "get-attached-file-url", 6 | deleteAttachedFile = "delete-attached-file", 7 | } 8 | -------------------------------------------------------------------------------- /file/src/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents a file that is attached to a resource. 3 | */ 4 | export type Attached = { 5 | /** 6 | * ARN of the resource that the file is attached to. 7 | * Could be a Connect Contact ARN or a Connect Case ARN. 8 | */ 9 | associatedResourceArn: string; 10 | }; 11 | 12 | /** 13 | * Base type for a file that is attached to a resource. 14 | */ 15 | export type Attachment = Attached & { 16 | /** 17 | * Identifier in Connect's File record 18 | */ 19 | fileId: string; 20 | }; 21 | 22 | export enum FileStatus { 23 | APPROVED = "APPROVED", 24 | REJECTED = "REJECTED", 25 | PROCESSING = "PROCESSING", 26 | PENDING_UPLOAD = "PENDING_UPLOAD", 27 | UPLOAD_EXPIRED = "UPLOAD_EXPIRED", 28 | FAILED = "FAILED", 29 | DELETED = "DELETED", 30 | } 31 | 32 | export type AttachmentMetadata = Attachment & { 33 | fileArn: string; 34 | fileName: string; 35 | fileStatus: FileStatus; 36 | fileSizeInBytes: number; 37 | creationTime: string; 38 | }; 39 | 40 | export type AttachmentError = { 41 | errorCode: string; 42 | errorMessage: string; 43 | fileId: string; 44 | }; 45 | 46 | export type BatchGetAttachedFileMetadataResponse = { 47 | files: AttachmentMetadata[]; 48 | errors: AttachmentError[]; 49 | }; 50 | 51 | /** 52 | * A group of files that are associated to the same resource 53 | */ 54 | export type RelatedAttachments = Attached & { 55 | fileIds: string[]; 56 | }; 57 | 58 | export type NewAttachment = Attached & { 59 | fileName: string; 60 | fileSizeInBytes: number; 61 | fileUseCaseType: "ATTACHMENT"; 62 | }; 63 | 64 | export type UploadableAttachment = Attachment & { 65 | /** 66 | * Include the file in a PUT request to this url 67 | */ 68 | uploadUrl: string; 69 | /** 70 | * Send these required headers along with the file to the uploadUrl. 71 | */ 72 | uploadHeaders: Record; 73 | /** 74 | * The upload request must be a PUT. 75 | */ 76 | uploadMethod: "PUT"; 77 | 78 | /** 79 | * The status of the file upload. 80 | */ 81 | fileStatus: FileStatus; 82 | }; 83 | 84 | export type DownloadableAttachment = AttachmentMetadata & { 85 | downloadUrl: string; 86 | }; 87 | -------------------------------------------------------------------------------- /file/tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "CommonJS", 5 | }, 6 | "exclude": ["./src/**/*.test.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /file/tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib-esm", 5 | }, 6 | "exclude": ["./src/**/*.test.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /file/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib/", 5 | }, 6 | "include": ["./src/**/*"] 7 | } 8 | -------------------------------------------------------------------------------- /message-template/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ["@babel/preset-env", { targets: { node: "current" } }], 4 | "@babel/preset-typescript", 5 | ], 6 | }; 7 | -------------------------------------------------------------------------------- /message-template/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@amazon-connect/message-template", 3 | "version": "1.0.6", 4 | "description": "Message template functionality of the Amazon Connect SDK", 5 | "publishConfig": { 6 | "access": "public" 7 | }, 8 | "scripts": { 9 | "build:esm": "tsc -p tsconfig.esm.json", 10 | "build:cjs": "tsc -p tsconfig.cjs.json", 11 | "build": "npm run build:esm && npm run build:cjs", 12 | "check-types": "tsc --noEmit", 13 | "clean": "rm -rf ./lib ./lib-esm ./build", 14 | "prepack": "npm run build", 15 | "test": "jest --coverage" 16 | }, 17 | "license": "Apache-2.0", 18 | "repository": { 19 | "type": "git", 20 | "url": "ssh://git.amazon.com/pkg/AmazonConnectSDK" 21 | }, 22 | "main": "lib/index.js", 23 | "module": "lib-esm/index.js", 24 | "types": "lib/index.d.ts", 25 | "files": [ 26 | "lib/**/*", 27 | "lib-esm/**/*" 28 | ], 29 | "dependencies": { 30 | "@amazon-connect/core": "1.0.6" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /message-template/src/index.ts: -------------------------------------------------------------------------------- 1 | export { MessageTemplateClient } from "./message-template-client"; 2 | export { messageTemplateNamespace } from "./message-template-namespace"; 3 | export { MessageTemplateRoute } from "./routes"; 4 | export * from "./types"; 5 | -------------------------------------------------------------------------------- /message-template/src/message-template-client.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/unbound-method */ 2 | import { ModuleContext, ModuleProxy } from "@amazon-connect/core"; 3 | import { mock } from "jest-mock-extended"; 4 | 5 | import { MessageTemplateClient } from "./message-template-client"; 6 | import { MessageTemplateRoute } from "./routes"; 7 | import { 8 | MessageTemplateContent, 9 | MessageTemplateEnabledState, 10 | SearchMessageTemplatesParams, 11 | SearchMessageTemplatesResponse, 12 | } from "./types"; 13 | 14 | const moduleProxyMock = mock(); 15 | const moduleContextMock = mock({ 16 | proxy: moduleProxyMock, 17 | }); 18 | 19 | let sut: MessageTemplateClient; 20 | 21 | beforeEach(jest.resetAllMocks); 22 | 23 | beforeEach(() => { 24 | sut = new MessageTemplateClient({ 25 | context: moduleContextMock, 26 | }); 27 | }); 28 | 29 | describe("MessageTemplateClient", () => { 30 | describe("searchMessageTemplates", () => { 31 | test("searchMessageTemplates returns message templates", async () => { 32 | const response: SearchMessageTemplatesResponse = 33 | mock(); 34 | moduleProxyMock.request.mockReturnValue( 35 | new Promise((resolve) => resolve(response)), 36 | ); 37 | const actualResult = await sut.searchMessageTemplates({ 38 | channels: ["EMAIL"], 39 | }); 40 | expect(moduleProxyMock.request).toHaveBeenCalledWith( 41 | MessageTemplateRoute.searchMessageTemplates, 42 | { request: { channels: ["EMAIL"] } }, 43 | ); 44 | expect(actualResult).toBe(response); 45 | }); 46 | 47 | test("searchMessageTemplates with filter returns message templates", async () => { 48 | const request: SearchMessageTemplatesParams = 49 | mock(); 50 | const response: SearchMessageTemplatesResponse = 51 | mock(); 52 | moduleProxyMock.request.mockReturnValue( 53 | new Promise((resolve) => resolve(response)), 54 | ); 55 | const actualResult = await sut.searchMessageTemplates(request); 56 | expect(moduleProxyMock.request).toHaveBeenCalledWith( 57 | MessageTemplateRoute.searchMessageTemplates, 58 | { request }, 59 | ); 60 | expect(actualResult).toBe(response); 61 | }); 62 | }); 63 | 64 | describe("isEnabled", () => { 65 | test("isEnabled returns enabled state type", async () => { 66 | const mockResponse = mock(); 67 | moduleProxyMock.request.mockReturnValue( 68 | new Promise((resolve) => resolve(mockResponse)), 69 | ); 70 | const actualResult = await sut.isEnabled(); 71 | expect(moduleProxyMock.request).toHaveBeenCalledWith( 72 | MessageTemplateRoute.isEnabled, 73 | ); 74 | expect(actualResult).toBe(mockResponse); 75 | }); 76 | }); 77 | 78 | describe("getContent", () => { 79 | test("getContent returns message template content", async () => { 80 | const messageTemplateId: string = "testMessageTemplateId"; 81 | const contactId: string = "testContactId"; 82 | const response: MessageTemplateContent = mock(); 83 | moduleProxyMock.request.mockReturnValue( 84 | new Promise((resolve) => resolve(response)), 85 | ); 86 | const actualResult = await sut.getContent(messageTemplateId, contactId); 87 | expect(moduleProxyMock.request).toHaveBeenCalledWith( 88 | MessageTemplateRoute.getContent, 89 | { messageTemplateId, contactId }, 90 | ); 91 | expect(actualResult).toBe(response); 92 | }); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /message-template/src/message-template-namespace.ts: -------------------------------------------------------------------------------- 1 | export const messageTemplateNamespace = "aws.connect.messageTemplate"; 2 | -------------------------------------------------------------------------------- /message-template/src/routes.ts: -------------------------------------------------------------------------------- 1 | export enum MessageTemplateRoute { 2 | searchMessageTemplates = "search-message-templates", 3 | getContent = "get-content", 4 | isEnabled = "is-enabled", 5 | } 6 | -------------------------------------------------------------------------------- /message-template/tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "CommonJS", 5 | }, 6 | "exclude": ["./src/**/*.test.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /message-template/tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib-esm", 5 | }, 6 | "exclude": ["./src/**/*.test.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /message-template/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib/", 5 | }, 6 | "include": ["./src/**/*"] 7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@amazon-connect/sdk", 3 | "version": "1.0.6", 4 | "description": "", 5 | "scripts": { 6 | "build": "npm run --ws build", 7 | "check-types": "npm run --ws check-types", 8 | "clean": "rm -rf ./build && npm run --ws clean", 9 | "format": "eslint . --fix", 10 | "lint": "eslint .", 11 | "prepare": "husky", 12 | "test": "npm run --ws build:cjs && npm run --ws check-types && npm run --ws test" 13 | }, 14 | "devDependencies": { 15 | "@babel/core": "7.25.2", 16 | "@babel/preset-env": "7.25.4", 17 | "@babel/preset-typescript": "7.24.7", 18 | "@types/jest": "29.5.12", 19 | "@typescript-eslint/eslint-plugin": "8.2.0", 20 | "@typescript-eslint/parser": "8.2.0", 21 | "babel-jest": "29.6.4", 22 | "eslint": "8.57.0", 23 | "eslint-config-prettier": "9.1.0", 24 | "eslint-plugin-import": "2.29.1", 25 | "eslint-plugin-jsx-a11y": "6.9.0", 26 | "eslint-plugin-prettier": "5.2.1", 27 | "eslint-plugin-simple-import-sort": "12.1.1", 28 | "husky": "9.1.5", 29 | "jest": "29.6.4", 30 | "jest-mock-extended": "3.0.7", 31 | "lint-staged": "15.2.9", 32 | "prettier": "3.3.3", 33 | "ts-loader": "9.5.1", 34 | "typescript": "5.5.4" 35 | }, 36 | "workspaces": [ 37 | "core", 38 | "workspace-types", 39 | "app", 40 | "site", 41 | "site-streams", 42 | "contact", 43 | "voice", 44 | "file", 45 | "email", 46 | "theme", 47 | "user", 48 | "message-template", 49 | "quick-responses" 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /quick-responses/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ["@babel/preset-env", { targets: { node: "current" } }], 4 | "@babel/preset-typescript", 5 | ], 6 | }; 7 | -------------------------------------------------------------------------------- /quick-responses/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@amazon-connect/quick-responses", 3 | "version": "1.0.6", 4 | "description": "Quick responses functionality of the Amazon Connect SDK", 5 | "publishConfig": { 6 | "access": "public" 7 | }, 8 | "scripts": { 9 | "build:esm": "tsc -p tsconfig.esm.json", 10 | "build:cjs": "tsc -p tsconfig.cjs.json", 11 | "build": "npm run build:esm && npm run build:cjs", 12 | "check-types": "tsc --noEmit", 13 | "clean": "rm -rf ./lib ./lib-esm ./build", 14 | "prepack": "npm run build", 15 | "test": "jest --coverage" 16 | }, 17 | "license": "Apache-2.0", 18 | "repository": { 19 | "type": "git", 20 | "url": "ssh://git.amazon.com/pkg/AmazonConnectSDK" 21 | }, 22 | "main": "lib/index.js", 23 | "module": "lib-esm/index.js", 24 | "types": "lib/index.d.ts", 25 | "files": [ 26 | "lib/**/*", 27 | "lib-esm/**/*" 28 | ], 29 | "dependencies": { 30 | "@amazon-connect/core": "1.0.6" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /quick-responses/src/index.ts: -------------------------------------------------------------------------------- 1 | export { QuickResponsesClient } from "./quick-responses-client"; 2 | export { quickResponsesNamespace } from "./quick-responses-namespace"; 3 | export { QuickResponsesRoute } from "./routes"; 4 | export { 5 | QuickResponseChannel, 6 | QuickResponseContents, 7 | QuickResponsesEnabledState, 8 | QuickResponsesQuery, 9 | QuickResponsesQueryFieldName, 10 | QuickResponsesQueryOperator, 11 | QuickResponsesQueryPriority, 12 | QuickResponsesSearchResultData, 13 | SearchQuickResponsesRequest, 14 | SearchQuickResponsesResult, 15 | } from "./types"; 16 | -------------------------------------------------------------------------------- /quick-responses/src/quick-responses-client.ts: -------------------------------------------------------------------------------- 1 | import { ConnectClient, ConnectClientConfig } from "@amazon-connect/core"; 2 | 3 | import { quickResponsesNamespace } from "./quick-responses-namespace"; 4 | import { QuickResponsesRoute } from "./routes"; 5 | import { 6 | QuickResponsesEnabledState, 7 | SearchQuickResponsesRequest, 8 | SearchQuickResponsesResult, 9 | } from "./types"; 10 | 11 | export class QuickResponsesClient extends ConnectClient { 12 | constructor(config?: ConnectClientConfig) { 13 | super(quickResponsesNamespace, config); 14 | } 15 | 16 | /** 17 | * Determine whether Quick Responses is enabled for this instance. 18 | * 19 | * If Quick Responses is enabled, returns the knowledge base id as well 20 | * 21 | * @returns {Promise} A promise that resolves to an object that indicates if the quick responses feature is enabled 22 | */ 23 | isEnabled(): Promise { 24 | return this.context.proxy.request(QuickResponsesRoute.isEnabled); 25 | } 26 | 27 | /** 28 | * Sends a request to retrieve a list of quick responses. 29 | * 30 | * @param {SearchQuickResponsesRequest} queryRequest request to search quick responses. 31 | * 32 | * @returns {Promise} A promise that resolves to an array of search results and a next token 33 | */ 34 | searchQuickResponses( 35 | queryRequest: SearchQuickResponsesRequest, 36 | ): Promise { 37 | return this.context.proxy.request( 38 | QuickResponsesRoute.searchQuickResponses, 39 | queryRequest, 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /quick-responses/src/quick-responses-namespace.ts: -------------------------------------------------------------------------------- 1 | export const quickResponsesNamespace = "aws.connect.quick-responses"; 2 | -------------------------------------------------------------------------------- /quick-responses/src/routes.ts: -------------------------------------------------------------------------------- 1 | export enum QuickResponsesRoute { 2 | isEnabled = "is-enabled", 3 | searchQuickResponses = "search-quick-responses", 4 | } 5 | -------------------------------------------------------------------------------- /quick-responses/tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "CommonJS", 5 | }, 6 | "exclude": ["./src/**/*.test.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /quick-responses/tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib-esm", 5 | }, 6 | "exclude": ["./src/**/*.test.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /quick-responses/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib/", 5 | }, 6 | "include": ["./src/**/*"] 7 | } 8 | -------------------------------------------------------------------------------- /site-streams/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ["@babel/preset-env", { targets: { node: "current" } }], 4 | "@babel/preset-typescript", 5 | ], 6 | }; 7 | -------------------------------------------------------------------------------- /site-streams/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@amazon-connect/site-streams", 3 | "version": "1.0.6", 4 | "description": "Used for building sites integrated with StreamsJS", 5 | "publishConfig": { 6 | "access": "public" 7 | }, 8 | "scripts": { 9 | "build:esm": "tsc -p tsconfig.esm.json", 10 | "build:cjs": "tsc -p tsconfig.cjs.json", 11 | "build": "npm run build:esm && npm run build:cjs", 12 | "check-types": "tsc --noEmit", 13 | "clean": "rm -rf ./lib ./lib-esm ./build", 14 | "prepack": "npm run build", 15 | "test": "jest --coverage" 16 | }, 17 | "license": "Apache-2.0", 18 | "repository": { 19 | "type": "git", 20 | "url": "ssh://git.amazon.com/pkg/AmazonConnectSDK" 21 | }, 22 | "main": "lib/index.js", 23 | "module": "lib-esm/index.js", 24 | "types": "lib/index.d.ts", 25 | "files": [ 26 | "lib/**/*", 27 | "lib-esm/**/*" 28 | ], 29 | "dependencies": { 30 | "@amazon-connect/site": "1.0.6" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /site-streams/src/amazon-connect-streams-site-config.ts: -------------------------------------------------------------------------------- 1 | import { AmazonConnectConfig } from "@amazon-connect/core"; 2 | 3 | export type AmazonConnectStreamsSiteConfig = AmazonConnectConfig & { 4 | instanceUrl: string; 5 | }; 6 | -------------------------------------------------------------------------------- /site-streams/src/amazon-connect-streams-site.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/unbound-method */ 2 | import { generateUUID } from "@amazon-connect/core"; 3 | import { 4 | getGlobalProvider, 5 | setGlobalProvider, 6 | } from "@amazon-connect/core/lib/provider/global-provider"; 7 | import { mocked } from "jest-mock"; 8 | import { mock } from "jest-mock-extended"; 9 | 10 | import { AmazonConnectStreamsSite } from "./amazon-connect-streams-site"; 11 | import { AmazonConnectStreamsSiteConfig } from "./amazon-connect-streams-site-config"; 12 | import { StreamsSiteProxy } from "./streams-site-proxy"; 13 | 14 | jest.mock("@amazon-connect/core/lib/utility/id-generator"); 15 | jest.mock("@amazon-connect/core/lib/provider/global-provider"); 16 | jest.mock("./streams-site-proxy"); 17 | 18 | const testProviderId = "testProviderId"; 19 | const config = mock(); 20 | 21 | beforeEach(jest.resetAllMocks); 22 | 23 | beforeEach(() => { 24 | mocked(generateUUID).mockReturnValueOnce(testProviderId); 25 | mocked(setGlobalProvider).mockImplementation(() => {}); 26 | }); 27 | 28 | afterEach(() => { 29 | AmazonConnectStreamsSite["isInitialized"] = false; 30 | }); 31 | 32 | describe("init", () => { 33 | let result: { 34 | provider: AmazonConnectStreamsSite; 35 | }; 36 | 37 | beforeEach(() => { 38 | result = AmazonConnectStreamsSite.init(config); 39 | }); 40 | 41 | test("should return AmazonConnectStreamsSite as provider", () => { 42 | expect(result.provider).toBeInstanceOf(AmazonConnectStreamsSite); 43 | }); 44 | 45 | test("should set the configuration", () => { 46 | const resultConfig = result.provider.config; 47 | 48 | expect(resultConfig).toEqual({ ...config }); 49 | }); 50 | 51 | test("should set random provider id", () => { 52 | expect(result.provider.id).toEqual(testProviderId); 53 | }); 54 | 55 | test("should set as global provider", () => { 56 | expect(setGlobalProvider).toBeCalledWith(result.provider); 57 | }); 58 | 59 | test("should create a StreamsSiteProxy", () => { 60 | expect(StreamsSiteProxy).toBeCalledTimes(1); 61 | const [proxyInstance] = mocked(StreamsSiteProxy).mock.instances; 62 | expect(StreamsSiteProxy).toBeCalledWith(result.provider); 63 | expect(result.provider.getProxy()).toBe(proxyInstance); 64 | }); 65 | 66 | test("should be initialized", () => { 67 | expect(AmazonConnectStreamsSite["isInitialized"]).toBeTruthy(); 68 | }); 69 | }); 70 | 71 | describe("default", () => { 72 | test("should return value from global provider", () => { 73 | const { provider } = AmazonConnectStreamsSite.init(config); 74 | mocked(getGlobalProvider).mockReturnValue(provider); 75 | 76 | const result = AmazonConnectStreamsSite.default; 77 | 78 | expect(result).toEqual(provider); 79 | }); 80 | }); 81 | 82 | describe("setCCPIframe", () => { 83 | test("should set the iframe on for the proxy", () => { 84 | const { provider: sut } = AmazonConnectStreamsSite.init(config); 85 | const iframe = mock(); 86 | 87 | sut.setCCPIframe(iframe); 88 | 89 | expect(StreamsSiteProxy).toBeCalledTimes(1); 90 | const [proxy] = mocked(StreamsSiteProxy).mock.instances; 91 | expect(proxy.setCCPIframe).toBeCalledWith(iframe); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /site-streams/src/amazon-connect-streams-site.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AmazonConnectProviderBase, 3 | getGlobalProvider, 4 | } from "@amazon-connect/core"; 5 | 6 | import { AmazonConnectStreamsSiteConfig } from "./amazon-connect-streams-site-config"; 7 | import { StreamsSiteProxy } from "./streams-site-proxy"; 8 | 9 | export class AmazonConnectStreamsSite extends AmazonConnectProviderBase { 10 | private constructor(config: AmazonConnectStreamsSiteConfig) { 11 | super({ 12 | config, 13 | proxyFactory: (p) => new StreamsSiteProxy(p as AmazonConnectStreamsSite), 14 | }); 15 | } 16 | 17 | static init(config: AmazonConnectStreamsSiteConfig): { 18 | provider: AmazonConnectStreamsSite; 19 | } { 20 | const provider = new AmazonConnectStreamsSite(config); 21 | 22 | AmazonConnectStreamsSite.initializeProvider(provider); 23 | 24 | return { provider }; 25 | } 26 | 27 | static get default(): AmazonConnectStreamsSite { 28 | return getGlobalProvider( 29 | "AmazonConnectStreamsSite has not been initialized", 30 | ); 31 | } 32 | 33 | setCCPIframe(iframe: HTMLIFrameElement): void { 34 | (this.getProxy() as StreamsSiteProxy).setCCPIframe(iframe); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /site-streams/src/index.ts: -------------------------------------------------------------------------------- 1 | export { AmazonConnectStreamsSite } from "./amazon-connect-streams-site"; 2 | export type { StreamsSiteMessageOrigin } from "./streams-site-message-origin"; 3 | -------------------------------------------------------------------------------- /site-streams/src/streams-site-message-origin.ts: -------------------------------------------------------------------------------- 1 | import { UpstreamMessageOrigin } from "@amazon-connect/core"; 2 | 3 | export type StreamsSiteMessageOrigin = UpstreamMessageOrigin & { 4 | _type: "streams-site"; 5 | providerId: string; 6 | origin: string; 7 | path: string; 8 | }; 9 | -------------------------------------------------------------------------------- /site-streams/src/streams-site-proxy.ts: -------------------------------------------------------------------------------- 1 | import { getOriginAndPath } from "@amazon-connect/core"; 2 | import { SiteProxy } from "@amazon-connect/site"; 3 | 4 | import { AmazonConnectStreamsSite } from "./amazon-connect-streams-site"; 5 | import { AmazonConnectStreamsSiteConfig } from "./amazon-connect-streams-site-config"; 6 | import { StreamsSiteMessageOrigin } from "./streams-site-message-origin"; 7 | 8 | export class StreamsSiteProxy extends SiteProxy { 9 | private ccpIFrame: HTMLIFrameElement | null; 10 | 11 | constructor(provider: AmazonConnectStreamsSite) { 12 | super(provider); 13 | 14 | this.ccpIFrame = null; 15 | } 16 | 17 | get proxyType(): string { 18 | return "streams-site"; 19 | } 20 | 21 | setCCPIframe(iframe: HTMLIFrameElement): void { 22 | const isCcpIFrameSet = Boolean(this.ccpIFrame); 23 | 24 | this.ccpIFrame = iframe; 25 | 26 | if (isCcpIFrameSet) this.resetConnection("CCP IFrame Updated"); 27 | } 28 | 29 | protected getUpstreamMessageOrigin(): StreamsSiteMessageOrigin { 30 | return { 31 | _type: "streams-site", 32 | providerId: this.provider.id, 33 | ...getOriginAndPath(), 34 | }; 35 | } 36 | 37 | protected verifyEventSource( 38 | evt: MessageEvent<{ type?: string | undefined }>, 39 | ): boolean { 40 | const ccpIFrame = this.ccpIFrame; 41 | 42 | if (!ccpIFrame) { 43 | this.proxyLogger.error( 44 | "CCP Iframe not provided to proxy. Unable to verify event to Connect to CCP.", 45 | { 46 | origin: evt.origin, 47 | }, 48 | ); 49 | return false; 50 | } 51 | 52 | const valid = evt.source === ccpIFrame.contentWindow; 53 | 54 | if (!valid) { 55 | this.proxyLogger.warn( 56 | "Message came from unexpected iframe. Not a valid CCP. Will not connect", 57 | { 58 | origin: evt.origin, 59 | }, 60 | ); 61 | } 62 | 63 | return valid; 64 | } 65 | 66 | protected invalidInitMessageHandler(): void { 67 | // CCP sends messages via Streams 68 | // Take no action here 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /site-streams/tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "CommonJS", 5 | }, 6 | "exclude": ["./src/**/*.test.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /site-streams/tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib-esm", 5 | }, 6 | "exclude": ["./src/**/*.test.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /site-streams/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib/", 5 | }, 6 | "include": ["./src/**/*"] 7 | } 8 | -------------------------------------------------------------------------------- /site/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ["@babel/preset-env", { targets: { node: "current" } }], 4 | "@babel/preset-typescript", 5 | ], 6 | }; 7 | -------------------------------------------------------------------------------- /site/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@amazon-connect/site", 3 | "version": "1.0.6", 4 | "description": "Site functionality of the Amazon Connect SDK", 5 | "publishConfig": { 6 | "access": "public" 7 | }, 8 | "scripts": { 9 | "build:esm": "tsc -p tsconfig.esm.json", 10 | "build:cjs": "tsc -p tsconfig.cjs.json", 11 | "build": "npm run build:esm && npm run build:cjs", 12 | "check-types": "tsc --noEmit", 13 | "clean": "rm -rf ./lib ./lib-esm ./build", 14 | "prepack": "npm run build", 15 | "test": "jest --coverage" 16 | }, 17 | "license": "Apache-2.0", 18 | "repository": { 19 | "type": "git", 20 | "url": "ssh://git.amazon.com/pkg/AmazonConnectSDK" 21 | }, 22 | "main": "lib/index.js", 23 | "module": "lib-esm/index.js", 24 | "types": "lib/index.d.ts", 25 | "files": [ 26 | "lib/**/*", 27 | "lib-esm/**/*" 28 | ], 29 | "dependencies": { 30 | "@amazon-connect/core": "1.0.6" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /site/src/index.ts: -------------------------------------------------------------------------------- 1 | export { SiteProxy } from "./proxy"; 2 | -------------------------------------------------------------------------------- /site/src/proxy/index.ts: -------------------------------------------------------------------------------- 1 | export { SiteProxy } from "./site-proxy"; 2 | -------------------------------------------------------------------------------- /site/tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "CommonJS", 5 | }, 6 | "exclude": ["./src/**/*.test.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /site/tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib-esm", 5 | }, 6 | "exclude": ["./src/**/*.test.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /site/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib/", 5 | }, 6 | "include": ["./src/**/*"] 7 | } 8 | -------------------------------------------------------------------------------- /theme/README.md: -------------------------------------------------------------------------------- 1 | # Amazon Connect SDK - Theme 2 | 3 | The theme package defines and applies the Connect theme when developing with Cloudscape. 4 | 5 | ## Installation 6 | 7 | ``` 8 | % npm install -P @amazon-connect/theme 9 | ``` 10 | 11 | ## Usage 12 | 13 | The theme package must be imported once at the entry point of the application. 14 | 15 | 16 | **src/index.ts** 17 | 18 | ``` 19 | import { applyConnectTheme } from "@amazon-connect/theme"; 20 | 21 | applyConnectTheme(); 22 | ``` 23 | 24 | From then on cloudscape components and design tokens can be used directly from Cloudscape. 25 | 26 | **src/App.ts** 27 | 28 | ``` 29 | import * as React from "react"; 30 | import Button from "@cloudscape-design/components/button"; 31 | 32 | export default () => { 33 | return ; 34 | } 35 | ``` 36 | -------------------------------------------------------------------------------- /theme/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ["@babel/preset-env", { targets: { node: "current" } }], 4 | "@babel/preset-typescript", 5 | ], 6 | }; 7 | -------------------------------------------------------------------------------- /theme/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: "jsdom", 3 | transformIgnorePatterns: ["/node_modules/"], 4 | }; 5 | -------------------------------------------------------------------------------- /theme/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@amazon-connect/theme", 3 | "version": "1.0.6", 4 | "description": "Theme for apps functionality of the Amazon Connect SDK", 5 | "publishConfig": { 6 | "access": "public" 7 | }, 8 | "scripts": { 9 | "build:esm": "tsc -p tsconfig.esm.json", 10 | "build:cjs": "tsc -p tsconfig.cjs.json", 11 | "build": "npm run build:esm && npm run build:cjs", 12 | "check-types": "tsc --noEmit", 13 | "clean": "rm -rf ./lib ./lib-esm ./build", 14 | "prepack": "npm run build", 15 | "test": "jest --coverage" 16 | }, 17 | "license": "Apache-2.0", 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/amazon-connect/AmazonConnectSDK.git" 21 | }, 22 | "main": "lib/index.js", 23 | "module": "lib-esm/index.js", 24 | "types": "lib/index.d.ts", 25 | "files": [ 26 | "lib/**/*", 27 | "lib-esm/**/*" 28 | ], 29 | "peerDependencies": { 30 | "@cloudscape-design/components": "^3.0.383" 31 | }, 32 | "devDependencies": { 33 | "jest-environment-jsdom": "^29.6.4", 34 | "jsdom": "^22.1.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /theme/src/build-theme.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from "@cloudscape-design/components/theming"; 2 | 3 | import { darkModeValues } from "./dark-mode-values"; 4 | import type { Overrides } from "./supported-overrides"; 5 | import { trimUndefinedValues } from "./trimUndefinedValues"; 6 | 7 | export function buildTheme({ 8 | fontFamily, 9 | brandColor, 10 | brandColorActive, 11 | lightBrandBackground, 12 | }: Overrides) { 13 | // Weird swap, but some states like disabling do have the "opposite" effect of highlighting 14 | const invertedHighlight = 15 | typeof brandColorActive === "string" 16 | ? brandColorActive 17 | : { light: brandColorActive?.dark, dark: brandColorActive?.light }; 18 | 19 | const theme: Theme = { 20 | tokens: { 21 | fontFamilyBase: fontFamily, 22 | 23 | colorBackgroundButtonNormalActive: { light: lightBrandBackground }, 24 | colorBackgroundButtonNormalHover: { light: lightBrandBackground }, 25 | colorBackgroundButtonPrimaryActive: brandColorActive, 26 | colorBackgroundButtonPrimaryDefault: brandColor, 27 | colorBackgroundButtonPrimaryHover: brandColorActive, 28 | colorBackgroundControlChecked: brandColor, 29 | colorBackgroundDropdownItemFilterMatch: { light: lightBrandBackground }, 30 | colorBackgroundItemSelected: { light: lightBrandBackground }, 31 | colorBackgroundLayoutToggleSelectedActive: brandColor, 32 | colorBackgroundLayoutToggleSelectedDefault: brandColor, 33 | colorBackgroundLayoutToggleSelectedHover: brandColorActive, 34 | colorBackgroundSegmentActive: brandColor, 35 | colorBackgroundToggleCheckedDisabled: invertedHighlight, 36 | // Not themable 37 | // colorBackgroundProgressBarContentDefault: brandColor, 38 | 39 | colorBorderButtonNormalActive: brandColorActive, 40 | colorBorderButtonNormalDefault: brandColor, 41 | colorBorderButtonNormalHover: brandColorActive, 42 | colorBorderItemFocused: brandColor, 43 | colorBorderItemSelected: brandColor, 44 | 45 | colorTextAccent: brandColor, 46 | colorTextButtonNormalActive: brandColorActive, 47 | colorTextButtonNormalDefault: brandColor, 48 | colorTextButtonNormalHover: brandColorActive, 49 | colorTextDropdownItemFilterMatch: brandColor, 50 | colorTextLayoutToggleHover: brandColor, 51 | colorTextLinkDefault: brandColor, 52 | colorTextLinkHover: brandColorActive, 53 | colorTextSegmentHover: brandColor, 54 | }, 55 | }; 56 | trimUndefinedValues(theme); 57 | 58 | theme.contexts = { 59 | "top-navigation": { tokens: darkModeValues(theme) }, 60 | header: { tokens: darkModeValues(theme) }, 61 | // Alerts remove branding colors from most elements, so we only have a few left 62 | alert: { 63 | tokens: { 64 | colorBackgroundControlChecked: brandColor, 65 | colorBackgroundDropdownItemFilterMatch: { light: lightBrandBackground }, 66 | colorBackgroundItemSelected: { light: lightBrandBackground }, 67 | colorBackgroundSegmentActive: brandColor, 68 | colorBackgroundToggleCheckedDisabled: invertedHighlight, 69 | 70 | colorTextAccent: brandColor, 71 | colorTextDropdownItemFilterMatch: brandColor, 72 | colorTextLinkDefault: brandColor, 73 | colorTextLinkHover: brandColorActive, 74 | colorTextSegmentHover: brandColor, 75 | }, 76 | }, 77 | // Nothing to override on flashbars, they should not include interactive elements that have brand colors. 78 | }; 79 | 80 | return theme; 81 | } 82 | -------------------------------------------------------------------------------- /theme/src/connect-constants.ts: -------------------------------------------------------------------------------- 1 | export const TEAL_100 = "#F2FAFC"; 2 | export const TEAL_200 = "#D6E6EA"; 3 | export const TEAL_300 = "#A7C1D1"; 4 | export const TEAL_400 = "#7CA2BB"; 5 | export const TEAL_500 = "#4A87AC"; 6 | export const TEAL_600 = "#077398"; 7 | export const TEAL_700 = "#065B78"; 8 | export const TEAL_800 = "#044A60"; 9 | export const TEAL_900 = "#033849"; 10 | -------------------------------------------------------------------------------- /theme/src/connect-overrides.ts: -------------------------------------------------------------------------------- 1 | import { 2 | TEAL_100, 3 | TEAL_300, 4 | TEAL_400, 5 | TEAL_600, 6 | TEAL_700, 7 | } from "./connect-constants"; 8 | import { Overrides } from "./supported-overrides"; 9 | 10 | export const CONNECT_OVERRIDES: Overrides = { 11 | brandColor: { light: TEAL_600, dark: TEAL_400 }, 12 | brandColorActive: { light: TEAL_700, dark: TEAL_300 }, 13 | lightBrandBackground: TEAL_100, 14 | }; 15 | -------------------------------------------------------------------------------- /theme/src/dark-mode-values.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from "@cloudscape-design/components/theming"; 2 | 3 | // Visual contexts need every one of our theme values re-applied, 4 | // and some of them are always dark so they need only our values for dark mode 5 | export function darkModeValues(theme: Theme): Theme["tokens"] { 6 | return Object.fromEntries( 7 | Object.entries(theme.tokens) 8 | .map(([token, value]) => [ 9 | token, 10 | typeof value === "string" ? value : value?.dark, 11 | ]) 12 | .filter( 13 | (entry): entry is [string, string] => typeof entry[1] !== "undefined", 14 | ), 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /theme/src/index.ts: -------------------------------------------------------------------------------- 1 | // Restore this once we support overrides. Not deleting as the goal is for it to come back soon. 2 | // export type { Overrides } from "./supported-overrides"; 3 | 4 | export { applyConnectTheme } from "./theming"; 5 | -------------------------------------------------------------------------------- /theme/src/merge-overrides.ts: -------------------------------------------------------------------------------- 1 | import { CONNECT_OVERRIDES } from "./connect-overrides"; 2 | import { 3 | Overrides, 4 | SupportedOverrides, 5 | validateToken, 6 | } from "./supported-overrides"; 7 | 8 | type TokenWithModes = keyof { 9 | [Token in keyof SupportedOverrides as SupportedOverrides[Token] extends string 10 | ? never 11 | : Token]: true; 12 | }; 13 | 14 | function hasMode(token: keyof SupportedOverrides): token is TokenWithModes { 15 | // Obviously this will need to evolve once we support theming border radius and other non-modal tokens. 16 | return token !== "fontFamily" && token !== "lightBrandBackground"; 17 | } 18 | 19 | export function mergeOverrides(overrides: Overrides) { 20 | const merged: Overrides = { ...CONNECT_OVERRIDES }; 21 | for (const [token, value] of Object.entries(overrides)) { 22 | if (!validateToken(token)) continue; 23 | if (typeof value === "string") { 24 | merged[token] = value; 25 | } else if (hasMode(token)) { 26 | const connectDefault = CONNECT_OVERRIDES[token]; 27 | if (typeof connectDefault === "undefined") { 28 | merged[token] = value; 29 | } else if (typeof connectDefault === "string") { 30 | merged[token] = { 31 | light: connectDefault, 32 | dark: connectDefault, 33 | ...value, 34 | }; 35 | } else { 36 | merged[token] = { ...connectDefault, ...value }; 37 | } 38 | } 39 | // Last else would be a non-modal token that received a modal value, which we simply ignore as invalid. 40 | } 41 | return merged; 42 | } 43 | -------------------------------------------------------------------------------- /theme/src/supported-overrides.ts: -------------------------------------------------------------------------------- 1 | export type ModeValues = { light: string; dark: string }; 2 | export type ModeDependentValue = string | Partial; 3 | 4 | export interface SupportedOverrides { 5 | /** 6 | * The default font family that will be applied globally to the product interface. 7 | */ 8 | fontFamily: string; 9 | /** 10 | * The primary brand color used for buttons, links and form controls. 11 | */ 12 | brandColor: ModeDependentValue; 13 | /** 14 | * Slightly lighter or darker version of the brand color, applied when brand color elements become active, 15 | * focused or hovered. 16 | */ 17 | brandColorActive: ModeDependentValue; 18 | /** 19 | * Very light version of the brand color, used as a background for selected items or light hover effects. 20 | * Does not apply in dark mode. 21 | */ 22 | lightBrandBackground: string; 23 | } 24 | 25 | export type Overrides = Partial; 26 | 27 | // Not much other choice than to duplicate this to have it available at runtime, unfortunately. 28 | const TOKENS = new Set([ 29 | "fontFamily", 30 | "brandColor", 31 | "brandColorActive", 32 | "lightBrandBackground", 33 | ]); 34 | 35 | export function validateToken( 36 | token: string, 37 | ): token is keyof SupportedOverrides { 38 | // Either we cast here or we type TOKENS as a set of strings, which is less safe against typos. 39 | return TOKENS.has(token as keyof SupportedOverrides); 40 | } 41 | -------------------------------------------------------------------------------- /theme/src/theming.test.ts: -------------------------------------------------------------------------------- 1 | import { applyTheme } from "@cloudscape-design/components/theming"; 2 | 3 | import { applyConnectTheme } from "./theming"; 4 | 5 | jest.mock("@cloudscape-design/components/theming"); 6 | const applyThemeMock = applyTheme as jest.MockedFn; 7 | 8 | // Not deleting this as it'll come back as soon as the theme override API is stable 9 | // function expectTokens(tokens: Theme["tokens"]) { 10 | // expect(applyThemeMock).toHaveBeenCalledWith({ 11 | // theme: { 12 | // // Need to cast as unknown because this whole repo uses @typescript-eslint/no-unsafe-assignment 13 | // // which produces false positives like this one. The typical recommendation is to instead rely on 14 | // // noImplicitAny from the Typescript compiler and no-explicit-any from ESLint. 15 | // tokens: expect.objectContaining(tokens) as unknown, 16 | // }, 17 | // }); 18 | // } 19 | 20 | describe("theming", () => { 21 | beforeEach(() => applyThemeMock.mockReset()); 22 | 23 | it("applies default Connect theme", () => { 24 | applyConnectTheme(); 25 | expect(applyThemeMock).toHaveBeenCalledTimes(1); 26 | // This is the only use-case that ever makes sense for snapshot tests. 27 | // We need an exact, precise output, and we want changes to it to be reviewed carefully. 28 | expect(applyThemeMock.mock.lastCall?.[0]).toMatchSnapshot(); 29 | }); 30 | 31 | // Not deleting these tests as they'll come back as soon as the theme override API is stable 32 | // it("allows non-modal overrides", () => { 33 | // applyConnectTheme({ fontFamily: "Comic Sans", brandColor: "hotpink" }); 34 | // expectTokens({ 35 | // fontFamilyBase: "Comic Sans", 36 | // colorBackgroundButtonPrimaryDefault: "hotpink", 37 | // colorBackgroundButtonPrimaryActive: { 38 | // dark: "#A7C1D1", 39 | // light: "#065B78", 40 | // }, 41 | // colorBackgroundButtonPrimaryHover: { 42 | // dark: "#A7C1D1", 43 | // light: "#065B78", 44 | // }, 45 | // }); 46 | // }); 47 | // 48 | // it("allows modal overrides", () => { 49 | // applyConnectTheme({ brandColor: { light: "deeppink", dark: "hotpink" } }); 50 | // expectTokens({ 51 | // colorBackgroundButtonPrimaryDefault: { 52 | // light: "deeppink", 53 | // dark: "hotpink", 54 | // }, 55 | // colorBackgroundButtonPrimaryActive: { 56 | // dark: "#A7C1D1", 57 | // light: "#065B78", 58 | // }, 59 | // colorBackgroundButtonPrimaryHover: { 60 | // dark: "#A7C1D1", 61 | // light: "#065B78", 62 | // }, 63 | // }); 64 | // }); 65 | // 66 | // it("ignores unknown tokens", () => { 67 | // applyConnectTheme(); 68 | // const defaultTheme = applyThemeMock.mock.lastCall; 69 | // // @ts-expect-error - We use a property that doesn't exist on purpose to mimic non-TS customers. 70 | // applyConnectTheme({ pageBorderRadius: "0" }); 71 | // expect(applyThemeMock.mock.lastCall).toEqual(defaultTheme); 72 | // }); 73 | // 74 | // it("ignores modal overrides on non-modal properties", () => { 75 | // applyConnectTheme(); 76 | // const defaultTheme = applyThemeMock.mock.lastCall; 77 | // // @ts-expect-error - We use the wrong value type for fontFamily to mimic non-TS customers. 78 | // applyConnectTheme({ fontFamily: { light: "deeppink", dark: "hotpink" } }); 79 | // expect(applyThemeMock.mock.lastCall).toEqual(defaultTheme); 80 | // }); 81 | }); 82 | -------------------------------------------------------------------------------- /theme/src/theming.ts: -------------------------------------------------------------------------------- 1 | import { applyTheme } from "@cloudscape-design/components/theming"; 2 | 3 | import { buildTheme } from "./build-theme"; 4 | import { CONNECT_OVERRIDES } from "./connect-overrides"; 5 | 6 | export function applyConnectTheme() { 7 | // Restore this once we support overrides. Not deleting as the goal is for it to come back soon. 8 | // const withConnectOverrides = overrides ? mergeOverrides(overrides) : CONNECT_OVERRIDES; 9 | applyTheme({ theme: buildTheme(CONNECT_OVERRIDES) }); 10 | } 11 | -------------------------------------------------------------------------------- /theme/src/trimUndefinedValues.ts: -------------------------------------------------------------------------------- 1 | import type { Theme } from "@cloudscape-design/components/theming"; 2 | 3 | export function trimUndefinedValues({ tokens }: Theme) { 4 | for (const key of Object.keys(tokens)) { 5 | // This is awkward but I'd rather not disable eslint's no-implicit-any in this file 6 | const typedKey = key as keyof Theme["tokens"]; 7 | if (typeof tokens[typedKey] === "undefined") { 8 | delete tokens[typedKey]; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /theme/tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "CommonJS", 5 | }, 6 | "exclude": ["./src/**/*.test.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /theme/tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib-esm", 5 | }, 6 | "exclude": ["./src/**/*.test.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /theme/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib/", 5 | }, 6 | "include": ["./src/**/*"], 7 | "typeAcquisition": { 8 | "include": ["jest"] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "declarationMap": true, 5 | "sourceMap": true, 6 | "strict": true, 7 | "noImplicitReturns": true, 8 | "noImplicitAny": true, 9 | "module": "es6", 10 | "moduleResolution": "node", 11 | "target": "es6", 12 | "allowJs": true, 13 | "allowSyntheticDefaultImports" : true, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /user/README.md: -------------------------------------------------------------------------------- 1 | # AmazonConnectSDK - user 2 | 3 | This module contains APIs to interact with the Amazon Connect user. 4 | -------------------------------------------------------------------------------- /user/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ["@babel/preset-env", { targets: { node: "current" } }], 4 | "@babel/preset-typescript", 5 | ], 6 | }; 7 | -------------------------------------------------------------------------------- /user/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@amazon-connect/user", 3 | "version": "1.0.6", 4 | "description": "User functionality of the Amazon Connect SDK", 5 | "publishConfig": { 6 | "access": "public" 7 | }, 8 | "scripts": { 9 | "build:esm": "tsc -p tsconfig.esm.json", 10 | "build:cjs": "tsc -p tsconfig.cjs.json", 11 | "build": "npm run build:esm && npm run build:cjs", 12 | "check-types": "tsc --noEmit", 13 | "clean": "rm -rf ./lib ./lib-esm ./build", 14 | "prepack": "npm run build", 15 | "test": "jest --coverage" 16 | }, 17 | "license": "Apache-2.0", 18 | "repository": { 19 | "type": "git", 20 | "url": "ssh://git.amazon.com/pkg/AmazonConnectSDK" 21 | }, 22 | "main": "lib/index.js", 23 | "module": "lib-esm/index.js", 24 | "types": "lib/index.d.ts", 25 | "files": [ 26 | "lib/**/*", 27 | "lib-esm/**/*" 28 | ], 29 | "dependencies": { 30 | "@amazon-connect/core": "1.0.6" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /user/src/index.ts: -------------------------------------------------------------------------------- 1 | export { userNamespace } from "./namespace"; 2 | export { UserRoutes } from "./routes"; 3 | export { SettingsClient } from "./settings-client"; 4 | export { UserTopicKey } from "./topic-keys"; 5 | export * from "./types"; 6 | -------------------------------------------------------------------------------- /user/src/namespace.ts: -------------------------------------------------------------------------------- 1 | export const userNamespace = "aws.connect.user"; 2 | -------------------------------------------------------------------------------- /user/src/routes.ts: -------------------------------------------------------------------------------- 1 | export enum UserRoutes { 2 | getLanguage = "getLanguage", 3 | } 4 | -------------------------------------------------------------------------------- /user/src/settings-client.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/unbound-method */ 2 | import { ModuleContext, ModuleProxy } from "@amazon-connect/core"; 3 | import { mock } from "jest-mock-extended"; 4 | 5 | import { UserRoutes } from "./routes"; 6 | import { SettingsClient } from "./settings-client"; 7 | import { UserTopicKey } from "./topic-keys"; 8 | 9 | const moduleProxyMock = mock(); 10 | const moduleContextMock = mock({ 11 | proxy: moduleProxyMock, 12 | }); 13 | 14 | let sut: SettingsClient; 15 | 16 | beforeEach(jest.resetAllMocks); 17 | 18 | describe("SettingsClient", () => { 19 | beforeEach(() => { 20 | sut = new SettingsClient({ context: moduleContextMock }); 21 | }); 22 | 23 | describe("Events", () => { 24 | test("onLanguageChanged adds subscription", () => { 25 | const handler = jest.fn(); 26 | 27 | sut.onLanguageChanged(handler); 28 | 29 | expect(moduleProxyMock.subscribe).toBeCalledWith( 30 | { key: UserTopicKey.LanguageChanged }, 31 | handler, 32 | ); 33 | }); 34 | 35 | test("offLanguageChanged removes subscription", () => { 36 | const handler = jest.fn(); 37 | 38 | sut.offLanguageChanged(handler); 39 | 40 | expect(moduleProxyMock.unsubscribe).toBeCalledWith( 41 | { key: UserTopicKey.LanguageChanged }, 42 | handler, 43 | ); 44 | }); 45 | }); 46 | 47 | describe("Requests", () => { 48 | test("getLanguage returns result", async () => { 49 | const language = "en_US"; 50 | moduleProxyMock.request.mockResolvedValueOnce({ language }); 51 | 52 | const actualResult = await sut.getLanguage(); 53 | 54 | expect(moduleProxyMock.request).toHaveBeenCalledWith( 55 | UserRoutes.getLanguage, 56 | ); 57 | expect(actualResult).toEqual(language); 58 | }); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /user/src/settings-client.ts: -------------------------------------------------------------------------------- 1 | import { ConnectClient, ConnectClientConfig } from "@amazon-connect/core"; 2 | 3 | import { userNamespace } from "./namespace"; 4 | import { UserRoutes } from "./routes"; 5 | import { UserTopicKey } from "./topic-keys"; 6 | import { Locale, UserLanguageChangedHandler } from "./types"; 7 | 8 | export class SettingsClient extends ConnectClient { 9 | constructor(config?: ConnectClientConfig) { 10 | super(userNamespace, config); 11 | } 12 | 13 | async getLanguage(): Promise { 14 | const { language } = await this.context.proxy.request<{ 15 | language: Locale | null; 16 | }>(UserRoutes.getLanguage); 17 | 18 | return language; 19 | } 20 | 21 | onLanguageChanged(handler: UserLanguageChangedHandler): void { 22 | this.context.proxy.subscribe( 23 | { key: UserTopicKey.LanguageChanged }, 24 | handler, 25 | ); 26 | } 27 | 28 | offLanguageChanged(handler: UserLanguageChangedHandler): void { 29 | this.context.proxy.unsubscribe( 30 | { key: UserTopicKey.LanguageChanged }, 31 | handler, 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /user/src/topic-keys.ts: -------------------------------------------------------------------------------- 1 | export enum UserTopicKey { 2 | LanguageChanged = "languageChange", 3 | } 4 | -------------------------------------------------------------------------------- /user/src/types.ts: -------------------------------------------------------------------------------- 1 | import { SubscriptionHandler } from "@amazon-connect/core"; 2 | 3 | export type Locale = 4 | | "en_US" 5 | | "de_DE" 6 | | "es_ES" 7 | | "fr_FR" 8 | | "ja_JP" 9 | | "it_IT" 10 | | "ko_KR" 11 | | "pt_BR" 12 | | "zh_CN" 13 | | "zh_TW"; 14 | 15 | export type UserLanguageChanged = { 16 | language: Locale; 17 | previous: { 18 | language: Locale | null; 19 | }; 20 | }; 21 | 22 | export type UserLanguageChangedHandler = 23 | SubscriptionHandler; 24 | -------------------------------------------------------------------------------- /user/tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "CommonJS", 5 | }, 6 | "exclude": ["./src/**/*.test.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /user/tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib-esm", 5 | }, 6 | "exclude": ["./src/**/*.test.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /user/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib/", 5 | }, 6 | "include": ["./src/**/*"], 7 | "typeAcquisition": { 8 | "include": ["jest"] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /voice/README.md: -------------------------------------------------------------------------------- 1 | # AmazonConnectSDK - voice 2 | 3 | This module contains APIs to interact with voice contact. 4 | -------------------------------------------------------------------------------- /voice/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ["@babel/preset-env", { targets: { node: "current" } }], 4 | "@babel/preset-typescript", 5 | ], 6 | }; 7 | -------------------------------------------------------------------------------- /voice/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@amazon-connect/voice", 3 | "version": "1.0.6", 4 | "description": "Voice functionality of the Amazon Connect SDK", 5 | "publishConfig": { 6 | "access": "public" 7 | }, 8 | "scripts": { 9 | "build:esm": "tsc -p tsconfig.esm.json", 10 | "build:cjs": "tsc -p tsconfig.cjs.json", 11 | "build": "npm run build:esm && npm run build:cjs", 12 | "check-types": "tsc --noEmit", 13 | "clean": "rm -rf ./lib ./lib-esm ./build", 14 | "prepack": "npm run build", 15 | "test": "jest --coverage" 16 | }, 17 | "license": "Apache-2.0", 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/amazon-connect/AmazonConnectSDK.git" 21 | }, 22 | "main": "lib/index.js", 23 | "module": "lib-esm/index.js", 24 | "types": "lib/index.d.ts", 25 | "files": [ 26 | "lib/**/*", 27 | "lib-esm/**/*" 28 | ], 29 | "dependencies": { 30 | "@amazon-connect/core": "1.0.6" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /voice/src/index.ts: -------------------------------------------------------------------------------- 1 | export { voiceNamespace } from "./namespace"; 2 | export * from "./routes"; 3 | export * from "./types"; 4 | export { VoiceClient } from "./voice-client"; 5 | -------------------------------------------------------------------------------- /voice/src/namespace.ts: -------------------------------------------------------------------------------- 1 | export const voiceNamespace = "aws.connect.voice"; 2 | -------------------------------------------------------------------------------- /voice/src/routes.ts: -------------------------------------------------------------------------------- 1 | export enum VoiceRoutes { 2 | getPhoneNumber = "contact/getPhoneNumber", 3 | getInitialCustomerPhoneNumber = "voice/getInitialCustomerPhoneNumber", 4 | listDialableCountries = "voice/listDialableCountries", 5 | createOutboundCall = "voice/createOutboundCall", 6 | getOutboundCallPermission = "voice/getOutboundCallPermission", 7 | } 8 | -------------------------------------------------------------------------------- /voice/src/types.ts: -------------------------------------------------------------------------------- 1 | export interface CreateOutboundCallOptions { 2 | queueARN?: string; 3 | relatedContactId?: string; 4 | } 5 | 6 | export type CreateOutboundCallResult = { 7 | contactId?: string; 8 | }; 9 | 10 | export type DialableCountry = { 11 | countryCode: string; 12 | callingCode: string; 13 | label: string; 14 | }; 15 | -------------------------------------------------------------------------------- /voice/src/voice-client.ts: -------------------------------------------------------------------------------- 1 | import { ConnectClient, ConnectClientConfig } from "@amazon-connect/core"; 2 | 3 | import { voiceNamespace } from "./namespace"; 4 | import { VoiceRoutes } from "./routes"; 5 | import { 6 | CreateOutboundCallOptions, 7 | CreateOutboundCallResult, 8 | DialableCountry, 9 | } from "./types"; 10 | 11 | export class VoiceClient extends ConnectClient { 12 | constructor(config?: ConnectClientConfig) { 13 | super(voiceNamespace, config); 14 | } 15 | /** 16 | * @deprecated Use `getInitialCustomerPhoneNumber` instead. 17 | */ 18 | async getPhoneNumber(contactId: string): Promise { 19 | const { phoneNumber } = await this.context.proxy.request<{ 20 | phoneNumber: string; 21 | }>(VoiceRoutes.getPhoneNumber, { 22 | contactId, 23 | }); 24 | return phoneNumber; 25 | } 26 | 27 | async getInitialCustomerPhoneNumber(contactId: string): Promise { 28 | const { phoneNumber } = await this.context.proxy.request<{ 29 | phoneNumber: string; 30 | }>(VoiceRoutes.getInitialCustomerPhoneNumber, { 31 | contactId, 32 | }); 33 | return phoneNumber; 34 | } 35 | 36 | listDialableCountries(): Promise { 37 | return this.context.proxy.request(VoiceRoutes.listDialableCountries); 38 | } 39 | 40 | async getOutboundCallPermission(): Promise { 41 | const { outboundCallPermission } = await this.context.proxy.request<{ 42 | outboundCallPermission: boolean; 43 | }>(VoiceRoutes.getOutboundCallPermission); 44 | return outboundCallPermission; 45 | } 46 | 47 | /** 48 | * @param phoneNumber phone number string in E.164 format 49 | */ 50 | createOutboundCall( 51 | phoneNumber: string, 52 | options?: CreateOutboundCallOptions, 53 | ): Promise { 54 | return this.context.proxy.request( 55 | VoiceRoutes.createOutboundCall, 56 | { 57 | phoneNumber, 58 | options, 59 | }, 60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /voice/tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "CommonJS", 5 | }, 6 | "exclude": ["./src/**/*.test.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /voice/tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib-esm", 5 | }, 6 | "exclude": ["./src/**/*.test.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /voice/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib/", 5 | }, 6 | "include": ["./src/**/*"] 7 | } 8 | -------------------------------------------------------------------------------- /workspace-types/README.md: -------------------------------------------------------------------------------- 1 | # AmazonConnectSDK - workspace-types 2 | 3 | This module contains types to be used within Agent Workspace applications. 4 | -------------------------------------------------------------------------------- /workspace-types/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ["@babel/preset-env", { targets: { node: "current" } }], 4 | "@babel/preset-typescript", 5 | ], 6 | }; 7 | -------------------------------------------------------------------------------- /workspace-types/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@amazon-connect/workspace-types", 3 | "version": "1.0.6", 4 | "description": "Workspace types for the Amazon Connect SDK", 5 | "publishConfig": { 6 | "access": "public" 7 | }, 8 | "scripts": { 9 | "build:esm": "tsc -p tsconfig.esm.json", 10 | "build:cjs": "tsc -p tsconfig.cjs.json", 11 | "build": "npm run build:esm && npm run build:cjs", 12 | "check-types": "tsc --noEmit", 13 | "clean": "rm -rf ./lib ./lib-esm ./build", 14 | "prepack": "npm run build", 15 | "test": "echo 'no tests'" 16 | }, 17 | "license": "Apache-2.0", 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/amazon-connect/AmazonConnectSDK.git" 21 | }, 22 | "main": "lib/index.js", 23 | "module": "lib-esm/index.js", 24 | "types": "lib/index.d.ts", 25 | "files": [ 26 | "lib/**/*", 27 | "lib-esm/**/*" 28 | ], 29 | "dependencies": { 30 | "@amazon-connect/core": "1.0.6" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /workspace-types/src/app-config.ts: -------------------------------------------------------------------------------- 1 | export type Permission = string; 2 | 3 | type BaseAppConfig = { 4 | arn: string; 5 | _type: string; 6 | namespace: string; 7 | id: string; 8 | name: string; 9 | description: string; 10 | accessUrl: string; 11 | permissions: Permission[]; 12 | }; 13 | 14 | export type IFrameAppConfig = BaseAppConfig & { 15 | _type: "iframe"; 16 | }; 17 | 18 | // This will provide support for additional types of apps 19 | // such as running an import map 20 | export type AppConfig = IFrameAppConfig; 21 | -------------------------------------------------------------------------------- /workspace-types/src/app-host-init-message.ts: -------------------------------------------------------------------------------- 1 | export type AppHostInitMessage = { 2 | type: "connect-app-host-init"; 3 | sdkVersion: string; 4 | providerId: string; 5 | }; 6 | -------------------------------------------------------------------------------- /workspace-types/src/app-message.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChildConnectionEnabledDownstreamMessage, 3 | ChildConnectionEnabledUpstreamMessage, 4 | ChildUpstreamMessage, 5 | SubscriptionHandlerData, 6 | SubscriptionTopic, 7 | UpstreamMessageOrigin, 8 | } from "@amazon-connect/core"; 9 | 10 | import { AppConfig } from "./app-config"; 11 | import { AppScope, ContactScope } from "./app-scope"; 12 | import { LifecycleStage } from "./lifecycle-stage"; 13 | 14 | export type LifecycleHandlerCompletedMessage = { 15 | type: "appLifecycleHandlerCompleted"; 16 | stage: LifecycleStage & ("create" | "destroy"); 17 | appInstanceId: string; 18 | }; 19 | 20 | export type AppPublishMessage = { 21 | type: "appPublish"; 22 | topic: SubscriptionTopic; 23 | data: SubscriptionHandlerData; 24 | messageOrigin?: UpstreamMessageOrigin; 25 | }; 26 | 27 | export type CloseAppMessage = { 28 | type: "closeApp"; 29 | isFatalError: boolean; 30 | message?: string; 31 | data?: Record | Error; 32 | }; 33 | 34 | export type AppUpstreamMessage = 35 | | ChildConnectionEnabledUpstreamMessage 36 | | ChildUpstreamMessage 37 | | AppPublishMessage 38 | | LifecycleHandlerCompletedMessage 39 | | CloseAppMessage; 40 | 41 | export type LifecycleMessage = { 42 | type: "appLifecycle"; 43 | stage: LifecycleStage; 44 | appInstanceId: string; 45 | appConfig: AppConfig; 46 | scope?: AppScope; 47 | 48 | /** 49 | * @deprecated Use `scope` instead. 50 | */ 51 | contactScope?: ContactScope; 52 | }; 53 | 54 | export type AppDownstreamMessage = 55 | | ChildConnectionEnabledDownstreamMessage 56 | | LifecycleMessage; 57 | -------------------------------------------------------------------------------- /workspace-types/src/app-scope.ts: -------------------------------------------------------------------------------- 1 | export type ContactScope = { 2 | type: "contact"; 3 | contactId: string; 4 | }; 5 | 6 | export type IdleScope = { 7 | type: "idle"; 8 | }; 9 | 10 | export type AppScope = ContactScope | IdleScope; 11 | -------------------------------------------------------------------------------- /workspace-types/src/index.ts: -------------------------------------------------------------------------------- 1 | export type { AppConfig, IFrameAppConfig } from "./app-config"; 2 | export type { AppHostInitMessage } from "./app-host-init-message"; 3 | export type * from "./app-message"; 4 | export type * from "./app-scope"; 5 | export type { LifecycleStage } from "./lifecycle-stage"; 6 | export type { AppMessageOrigin } from "./message-origin"; 7 | -------------------------------------------------------------------------------- /workspace-types/src/lifecycle-stage.ts: -------------------------------------------------------------------------------- 1 | export type LifecycleStage = "create" | "start" | "stop" | "destroy"; 2 | -------------------------------------------------------------------------------- /workspace-types/src/message-origin.ts: -------------------------------------------------------------------------------- 1 | import { UpstreamMessageOrigin } from "@amazon-connect/core"; 2 | 3 | export type AppMessageOrigin = UpstreamMessageOrigin & { 4 | _type: "app"; 5 | origin: string; 6 | path: string; 7 | }; 8 | -------------------------------------------------------------------------------- /workspace-types/tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "CommonJS", 5 | }, 6 | "exclude": ["./src/**/*.test.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /workspace-types/tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib-esm", 5 | }, 6 | "exclude": ["./src/**/*.test.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /workspace-types/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib/", 5 | }, 6 | "include": ["./src/**/*"], 7 | "typeAcquisition": { 8 | "include": ["jest"] 9 | } 10 | } 11 | --------------------------------------------------------------------------------