From f761154ead8f303194a7ff9747aa5635238c13fe Mon Sep 17 00:00:00 2001 From: Godwin Alexander Date: Mon, 21 Aug 2023 13:13:40 +0100 Subject: [PATCH] feat: glee auth v2 (#474) Co-authored-by: Khuda Dad Nomani --- .prettierignore | 1 + docs/reference/README.md | 2 + docs/reference/classes/lib_glee.default.md | 6 +- .../interfaces/lib.HttpAuthConfig.md | 12 + docs/reference/interfaces/lib.WsAuthConfig.md | 22 ++ docs/reference/modules/lib.md | 4 + docs/reference/modules/lib_adapter.md | 20 ++ docs/reference/modules/lib_userAuth.md | 85 ++++++ docs/reference/modules/lib_wsHttpAuth.md | 9 + examples/anime-http/client/.env | 3 + examples/anime-http/client/asyncapi.yaml | 37 +++ .../anime-http/client/auth/trendingAnime.ts | 15 ++ examples/anime-http/client/docs/asyncapi.md | 181 +++++++++++++ examples/anime-http/server/asyncapi.yaml | 43 +++- .../server/auth/trendingAnimeServer.ts | 25 ++ examples/anime-http/server/docs/asyncapi.md | 164 ++++++++++++ .../crypto-websockets/client/asyncapi.yaml | 19 ++ .../client/auth/websockets.ts | 8 + examples/crypto-websockets/client/db.json | 2 +- .../crypto-websockets/client/docs/asyncapi.md | 56 ++++ .../crypto-websockets/server/asyncapi.yaml | 24 +- .../server/auth/websocket.ts | 22 ++ .../crypto-websockets/server/docs/asyncapi.md | 88 +++++++ .../websocket-server/docs/asyncapi.md | 123 +++++++++ jest.config.js | 6 +- package.json | 6 - src/adapters/http/client.ts | 37 ++- src/adapters/http/server.ts | 55 +++- src/adapters/ws/client.ts | 25 +- src/adapters/ws/server.ts | 60 ++++- src/index.ts | 38 ++- src/lib/adapter.ts | 34 ++- src/lib/configs.ts | 9 +- src/lib/glee.ts | 7 +- src/lib/index.d.ts | 30 +++ src/lib/userAuth.ts | 79 ++++++ src/lib/wsHttpAuth.ts | 180 +++++++++++++ test/lib/adapter.test.ts | 243 +++++++++++------- 38 files changed, 1629 insertions(+), 151 deletions(-) create mode 100644 docs/reference/modules/lib_userAuth.md create mode 100644 docs/reference/modules/lib_wsHttpAuth.md create mode 100644 examples/anime-http/client/.env create mode 100644 examples/anime-http/client/auth/trendingAnime.ts create mode 100644 examples/anime-http/client/docs/asyncapi.md create mode 100644 examples/anime-http/server/auth/trendingAnimeServer.ts create mode 100644 examples/anime-http/server/docs/asyncapi.md create mode 100644 examples/crypto-websockets/client/auth/websockets.ts create mode 100644 examples/crypto-websockets/client/docs/asyncapi.md create mode 100644 examples/crypto-websockets/server/auth/websocket.ts create mode 100644 examples/crypto-websockets/server/docs/asyncapi.md create mode 100644 examples/social-network/websocket-server/docs/asyncapi.md create mode 100644 src/lib/userAuth.ts create mode 100644 src/lib/wsHttpAuth.ts diff --git a/.prettierignore b/.prettierignore index 8b1c374f5..764aff77b 100644 --- a/.prettierignore +++ b/.prettierignore @@ -3,3 +3,4 @@ coverage .github examples node_modules +docs diff --git a/docs/reference/README.md b/docs/reference/README.md index fac47330e..45f8c9a10 100644 --- a/docs/reference/README.md +++ b/docs/reference/README.md @@ -35,7 +35,9 @@ - [lib/message](modules/lib_message.md) - [lib/router](modules/lib_router.md) - [lib/servers](modules/lib_servers.md) +- [lib/userAuth](modules/lib_userAuth.md) - [lib/util](modules/lib_util.md) +- [lib/wsHttpAuth](modules/lib_wsHttpAuth.md) - [middlewares](modules/middlewares.md) - [middlewares/buffer2string](modules/middlewares_buffer2string.md) - [middlewares/errorLogger](modules/middlewares_errorLogger.md) diff --git a/docs/reference/classes/lib_glee.default.md b/docs/reference/classes/lib_glee.default.md index 564c0c247..079a23132 100644 --- a/docs/reference/classes/lib_glee.default.md +++ b/docs/reference/classes/lib_glee.default.md @@ -324,9 +324,9 @@ Adds a connection adapter. | :------ | :------ | | `Adapter` | typeof [`default`](lib_adapter.default.md) | | `«destructured»` | `Object` | -| › `parsedAsyncAPI` | `AsyncAPIDocument` | -| › `server` | `Server` | -| › `serverName` | `string` | +| › `parsedAsyncAPI` | `AsyncAPIDocument` | +| › `server` | `Server` | +| › `serverName` | `string` | #### Returns diff --git a/docs/reference/interfaces/lib.HttpAuthConfig.md b/docs/reference/interfaces/lib.HttpAuthConfig.md index b712b351a..63fafab65 100644 --- a/docs/reference/interfaces/lib.HttpAuthConfig.md +++ b/docs/reference/interfaces/lib.HttpAuthConfig.md @@ -8,10 +8,22 @@ ### Properties +- [password](lib.HttpAuthConfig.md#password) - [token](lib.HttpAuthConfig.md#token) +- [username](lib.HttpAuthConfig.md#username) ## Properties +### password + +• `Optional` **password**: `string` + +#### Defined in + +[src/lib/index.d.ts:35](https://github.com/oviecodes/glee/blob/2283982/src/lib/index.d.ts#L35) + +___ + ### token • `Optional` **token**: `string` diff --git a/docs/reference/interfaces/lib.WsAuthConfig.md b/docs/reference/interfaces/lib.WsAuthConfig.md index 3516741dc..c12bee260 100644 --- a/docs/reference/interfaces/lib.WsAuthConfig.md +++ b/docs/reference/interfaces/lib.WsAuthConfig.md @@ -8,10 +8,22 @@ ### Properties +- [password](lib.WsAuthConfig.md#password) - [token](lib.WsAuthConfig.md#token) +- [username](lib.WsAuthConfig.md#username) ## Properties +### password + +• `Optional` **password**: `string` + +#### Defined in + +[src/lib/index.d.ts:29](https://github.com/oviecodes/glee/blob/2283982/src/lib/index.d.ts#L29) + +___ + ### token • `Optional` **token**: `string` @@ -19,3 +31,13 @@ #### Defined in [src/lib/index.d.ts:27](https://github.com/asyncapi/glee/blob/b84101b/src/lib/index.d.ts#L27) + +___ + +### username + +• `Optional` **username**: `string` + +#### Defined in + +[src/lib/index.d.ts:28](https://github.com/oviecodes/glee/blob/2283982/src/lib/index.d.ts#L28) diff --git a/docs/reference/modules/lib.md b/docs/reference/modules/lib.md index 4b8ba6bd0..e5c5d999f 100644 --- a/docs/reference/modules/lib.md +++ b/docs/reference/modules/lib.md @@ -14,7 +14,10 @@ ### Type Aliases - [AuthFunction](lib.md#authfunction) +- [AuthProps](lib.md#authprops) - [CoreGleeConfig](lib.md#coregleeconfig) +- [GleeAuthFunction](lib.md#gleeauthfunction) +- [GleeAuthFunctionEvent](lib.md#gleeauthfunctionevent) - [GleeClusterAdapterConfig](lib.md#gleeclusteradapterconfig) - [GleeConfig](lib.md#gleeconfig) - [GleeFunction](lib.md#gleefunction) @@ -30,6 +33,7 @@ - [QueryParam](lib.md#queryparam) - [WebSocketServerType](lib.md#websocketservertype) - [WebsocketAdapterConfig](lib.md#websocketadapterconfig) +- [WsHttpAuth](lib.md#wshttpauth) ## Type Aliases diff --git a/docs/reference/modules/lib_adapter.md b/docs/reference/modules/lib_adapter.md index 3adeff2d2..a43b213a6 100644 --- a/docs/reference/modules/lib_adapter.md +++ b/docs/reference/modules/lib_adapter.md @@ -10,10 +10,30 @@ ### Type Aliases +- [AuthEvent](lib_adapter.md#authevent) - [EnrichedEvent](lib_adapter.md#enrichedevent) ## Type Aliases +### AuthEvent + +Ƭ **AuthEvent**: `Object` + +#### Type declaration + +| Name | Type | +| :------ | :------ | +| `authProps` | [`AuthProps`](lib.md#authprops) | +| `callback` | `any` | +| `doc` | `any` | +| `serverName` | `string` | + +#### Defined in + +[src/lib/adapter.ts:17](https://github.com/oviecodes/glee/blob/2283982/src/lib/adapter.ts#L17) + +___ + ### EnrichedEvent Ƭ **EnrichedEvent**: `Object` diff --git a/docs/reference/modules/lib_userAuth.md b/docs/reference/modules/lib_userAuth.md new file mode 100644 index 000000000..cea31f082 --- /dev/null +++ b/docs/reference/modules/lib_userAuth.md @@ -0,0 +1,85 @@ +[@asyncapi/glee](../README.md) / lib/userAuth + +# Module: lib/userAuth + +## Table of contents + +### Variables + +- [authFunctions](lib_userAuth.md#authfunctions) + +### Functions + +- [clientAuthConfig](lib_userAuth.md#clientauthconfig) +- [register](lib_userAuth.md#register) +- [triggerAuth](lib_userAuth.md#triggerauth) + +## Variables + +### authFunctions + +• `Const` **authFunctions**: `Map`<`string`, `AuthFunctionInfo`\> + +#### Defined in + +[src/lib/userAuth.ts:15](https://github.com/oviecodes/glee/blob/2283982/src/lib/userAuth.ts#L15) + +## Functions + +### clientAuthConfig + +▸ **clientAuthConfig**(`serverName`): `Promise`<[`GleeAuthFunction`](lib.md#gleeauthfunction)\> + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `serverName` | `string` | + +#### Returns + +`Promise`<[`GleeAuthFunction`](lib.md#gleeauthfunction)\> + +#### Defined in + +[src/lib/userAuth.ts:79](https://github.com/oviecodes/glee/blob/2283982/src/lib/userAuth.ts#L79) + +___ + +### register + +▸ **register**(`dir`): `Promise`<`void`[]\> + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `dir` | `string` | + +#### Returns + +`Promise`<`void`[]\> + +#### Defined in + +[src/lib/userAuth.ts:17](https://github.com/oviecodes/glee/blob/2283982/src/lib/userAuth.ts#L17) + +___ + +### triggerAuth + +▸ **triggerAuth**(`params`): `Promise`<`void`\> + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `params` | [`GleeAuthFunctionEvent`](lib.md#gleeauthfunctionevent) | + +#### Returns + +`Promise`<`void`\> + +#### Defined in + +[src/lib/userAuth.ts:49](https://github.com/oviecodes/glee/blob/2283982/src/lib/userAuth.ts#L49) diff --git a/docs/reference/modules/lib_wsHttpAuth.md b/docs/reference/modules/lib_wsHttpAuth.md new file mode 100644 index 000000000..4bddd43be --- /dev/null +++ b/docs/reference/modules/lib_wsHttpAuth.md @@ -0,0 +1,9 @@ +[@asyncapi/glee](../README.md) / lib/wsHttpAuth + +# Module: lib/wsHttpAuth + +## Table of contents + +### Classes + +- [default](../classes/lib_wsHttpAuth.default.md) diff --git a/examples/anime-http/client/.env b/examples/anime-http/client/.env new file mode 100644 index 000000000..f04bbcf20 --- /dev/null +++ b/examples/anime-http/client/.env @@ -0,0 +1,3 @@ +TOKEN=arb-valueFromClientAuth +OAUTH2=arb-OAUTH_access_token +APIKEY=arb-APIKEY_token \ No newline at end of file diff --git a/examples/anime-http/client/asyncapi.yaml b/examples/anime-http/client/asyncapi.yaml index d98da36e2..2ea9787da 100644 --- a/examples/anime-http/client/asyncapi.yaml +++ b/examples/anime-http/client/asyncapi.yaml @@ -7,6 +7,14 @@ servers: trendingAnime: url: http://localhost:8081 protocol: http + security: + - token: [] + - userPass: [] + - apiKey: [] + - UserOrPassKey: [] + - oauth: + - write:pets + - read:pets testwebhook: url: ws://localhost:9000 protocol: ws @@ -43,3 +51,32 @@ components: summary: ping client payload: type: object + securitySchemes: + token: + type: http + scheme: bearer + bearerFormat: JWT + userPass: + type: userPassword + apiKey: + type: httpApiKey + name: api_key + in: query + UserOrPassKey: + type: apiKey + in: user + oauth: + type: oauth2 + flows: + implicit: + authorizationUrl: https://example.com/api/oauth/dialog + scopes: + write:pets: modify pets in your account + read:pets: read your pets + authorizationCode: + authorizationUrl: https://example.com/api/oauth/dialog + tokenUrl: https://example.com/api/oauth/dialog + scopes: + delete:pets: modify pets in your account + update:pets: read your pets + diff --git a/examples/anime-http/client/auth/trendingAnime.ts b/examples/anime-http/client/auth/trendingAnime.ts new file mode 100644 index 000000000..eb93b81a6 --- /dev/null +++ b/examples/anime-http/client/auth/trendingAnime.ts @@ -0,0 +1,15 @@ +/* eslint-disable no-undef */ + +export async function clientAuth({ serverName }) { + console.log("serverName", serverName) + + return { + token: process.env.TOKEN, + oauth: process.env.OAUTH2, + apiKey: process.env.APIKEY, + userPass: { + user: process.env.USERNAME, + password: process.env.PASSWORD + } + } + } \ No newline at end of file diff --git a/examples/anime-http/client/docs/asyncapi.md b/examples/anime-http/client/docs/asyncapi.md new file mode 100644 index 000000000..1182c0e8f --- /dev/null +++ b/examples/anime-http/client/docs/asyncapi.md @@ -0,0 +1,181 @@ +# AsyncAPI IMDB client 1.0.0 documentation + +This app creates a client that subscribes to the server for getting the top 10 trending/upcoming anime. + +## Table of Contents + +* [Servers](#servers) + * [trendingAnime](#trendinganime-server) + * [testwebhook](#testwebhook-server) +* [Operations](#operations) + * [PUB /test](#pub-test-operation) + * [PUB trendingAnime](#pub-trendinganime-operation) + * [SUB trendingAnime](#sub-trendinganime-operation) + +## Servers + +### `trendingAnime` Server + +* URL: `http://localhost:8081` +* Protocol: `http` + + +#### Security + +##### Security Requirement 1 + +* Type: `HTTP` + * Scheme: bearer + * Bearer format: JWT + + + + +##### Security Requirement 2 + +* Type: `User/Password` + + + +##### Security Requirement 3 + +* Type: `HTTP API key` + * Name: api_key + * In: query + + + + +##### Security Requirement 4 + +* Type: `API key` + * In: user + + + + +##### Security Requirement 5 + +* Type: `OAuth2` + * Flows: + + Required scopes: `write:pets`, `read:pets` + + | Flow | Auth URL | Token URL | Refresh URL | Scopes | + |---|---|---|---|---| + | Implicit | [https://example.com/api/oauth/dialog](https://example.com/api/oauth/dialog) | - | - | `write:pets`, `read:pets` | + | Authorization Code | [https://example.com/api/oauth/dialog](https://example.com/api/oauth/dialog) | [https://example.com/api/oauth/dialog](https://example.com/api/oauth/dialog) | - | `delete:pets`, `update:pets` | + + + + + + + + + +### `testwebhook` Server + +* URL: `ws://localhost:9000` +* Protocol: `ws` + + + +## Operations + +### PUB `/test` Operation + +* Operation ID: `index` + +#### `ws` Channel specific information + +| Name | Type | Description | Value | Constraints | Notes | +|---|---|---|---|---|---| +| bindingVersion | - | - | `"0.1.0"` | - | - | + +#### Message `test` + +*ping client* + +##### Payload + +| Name | Type | Description | Value | Constraints | Notes | +|---|---|---|---|---|---| +| (root) | object | - | - | - | **additional properties are allowed** | + +> Examples of payload _(generated)_ + +```json +{} +``` + + + +### PUB `trendingAnime` Operation + +* Operation ID: `trendingAnimeListRecieverController` +* Available only on servers: [trendingAnime](#trendinganime-server) + +#### `http` Channel specific information + +| Name | Type | Description | Value | Constraints | Notes | +|---|---|---|---|---|---| +| type | - | - | `"request"` | - | - | +| method | - | - | `"POST"` | - | - | +| bindingVersion | - | - | `"0.1.0"` | - | - | + +#### Message `` + +*Data required to populate trending anime* + +##### Payload + +| Name | Type | Description | Value | Constraints | Notes | +|---|---|---|---|---|---| +| (root) | object | - | - | - | **additional properties are allowed** | +| name | string | Name of the anime. | - | - | **required** | +| rating | string | Rating of the show. | - | - | **required** | +| genre | string | The genre of anime. | - | - | **required** | +| studio | string | The studio of anime. | - | - | **required** | + +> Examples of payload _(generated)_ + +```json +{ + "name": "string", + "rating": "string", + "genre": "string", + "studio": "string" +} +``` + + + +### SUB `trendingAnime` Operation + +* Available only on servers: [trendingAnime](#trendinganime-server) + +#### `http` Channel specific information + +| Name | Type | Description | Value | Constraints | Notes | +|---|---|---|---|---|---| +| type | - | - | `"request"` | - | - | +| method | - | - | `"POST"` | - | - | +| bindingVersion | - | - | `"0.1.0"` | - | - | + +#### Message `` + +##### Payload + +| Name | Type | Description | Value | Constraints | Notes | +|---|---|---|---|---|---| +| (root) | object | - | - | - | **additional properties are allowed** | + +> Examples of payload _(generated)_ + +```json +{} +``` + + + diff --git a/examples/anime-http/server/asyncapi.yaml b/examples/anime-http/server/asyncapi.yaml index 7601f29b6..9930a3838 100644 --- a/examples/anime-http/server/asyncapi.yaml +++ b/examples/anime-http/server/asyncapi.yaml @@ -1,12 +1,19 @@ -asyncapi: 2.4.0 +asyncapi: 2.6.0 info: title: AsyncAPI IMDB server version: 1.0.0 description: This app is a dummy server that would stream the trending/upcoming anime. servers: trendingAnimeServer: - url: http://localhost:8081 + url: 'http://localhost:8081' protocol: http + security: + - token: [] + - userPass: [] + - apiKey: [] + - UserOrPassKey: [] + - cert: [] + - oauth: [] channels: trendingAnime: bindings: @@ -15,13 +22,13 @@ channels: method: POST bindingVersion: 0.1.0 query: - $ref: "#/components/schemas/request" + $ref: '#/components/schemas/request' body: - $ref: "#/components/schemas/request" + $ref: '#/components/schemas/request' publish: operationId: trendingAnimeController message: - $ref: "#/components/messages/trendingAnime" + $ref: '#/components/messages/trendingAnime' subscribe: message: payload: @@ -31,7 +38,7 @@ components: trendingAnime: summary: Data required to populate trending anime payload: - $ref: "#/components/schemas/request" + $ref: '#/components/schemas/request' schemas: request: type: object @@ -53,3 +60,27 @@ components: studio: type: string description: The studio of anime. + securitySchemes: + token: + type: http + scheme: bearer + bearerFormat: JWT + userPass: + type: userPassword + apiKey: + type: httpApiKey + name: api_key + in: query + UserOrPassKey: + type: apiKey + in: user + cert: + type: X509 + oauth: + type: oauth2 + flows: + clientCredentials: + tokenUrl: https://example.com/api/oauth/dialog + scopes: + delete:pets: modify pets in your account + update:pets: read your pets \ No newline at end of file diff --git a/examples/anime-http/server/auth/trendingAnimeServer.ts b/examples/anime-http/server/auth/trendingAnimeServer.ts new file mode 100644 index 000000000..7773225a3 --- /dev/null +++ b/examples/anime-http/server/auth/trendingAnimeServer.ts @@ -0,0 +1,25 @@ +// /* eslint-disable no-undef */ + +// //@ts-ignore +import axios from "axios" + +export async function serverAuth({ authProps, done }) { + await axios.get("https://jsonplaceholder.typicode.com/todos/1", { + timeout: 5000, + }) + + console.log("oauth props", authProps.getOauthToken()) + console.log("httpAPIkey", authProps.getHttpAPIKeys("api_key")) + console.log("token", authProps.getToken()) + console.log("userpassword", authProps.getUserPass()) + + done(false) +} + +export async function clientAuth({ serverName }) { + if (serverName === "websockets") { + return { + token: process.env.TOKEN, + } + } +} diff --git a/examples/anime-http/server/docs/asyncapi.md b/examples/anime-http/server/docs/asyncapi.md new file mode 100644 index 000000000..41c10f8dc --- /dev/null +++ b/examples/anime-http/server/docs/asyncapi.md @@ -0,0 +1,164 @@ +# AsyncAPI IMDB server 1.0.0 documentation + +This app is a dummy server that would stream the trending/upcoming anime. + +## Table of Contents + +* [Servers](#servers) + * [trendingAnimeServer](#trendinganimeserver-server) +* [Operations](#operations) + * [PUB trendingAnime](#pub-trendinganime-operation) + * [SUB trendingAnime](#sub-trendinganime-operation) + +## Servers + +### `trendingAnimeServer` Server + +* URL: `http://localhost:8081` +* Protocol: `http` + + +#### Security + +##### Security Requirement 1 + +* Type: `HTTP` + * Scheme: bearer + * Bearer format: JWT + + + + +##### Security Requirement 2 + +* Type: `User/Password` + + + +##### Security Requirement 3 + +* Type: `HTTP API key` + * Name: api_key + * In: query + + + + +##### Security Requirement 4 + +* Type: `API key` + * In: user + + + + +##### Security Requirement 5 + +* Type: `X509` + + + +##### Security Requirement 6 + +* Type: `OAuth2` + * Flows: + + | Flow | Auth URL | Token URL | Refresh URL | Scopes | + |---|---|---|---|---| + | Client credentials | - | [https://example.com/api/oauth/dialog](https://example.com/api/oauth/dialog) | - | `delete:pets`, `update:pets` | + + + + + + + + + +## Operations + +### PUB `trendingAnime` Operation + +* Operation ID: `trendingAnimeController` + +#### `http` Channel specific information + +| Name | Type | Description | Value | Constraints | Notes | +|---|---|---|---|---|---| +| type | - | - | `"request"` | - | - | +| method | - | - | `"POST"` | - | - | +| bindingVersion | - | - | `"0.1.0"` | - | - | +| query | object | - | - | - | **additional properties are allowed** | +| query.name | string | Name of the anime. | - | - | **required** | +| query.rating | string | Rating of the show. | - | - | **required** | +| query.genre | string | The genre of anime. | - | - | **required** | +| query.studio | string | The studio of anime. | - | - | **required** | +| body | object | - | - | - | **additional properties are allowed** | +| body.name | string | Name of the anime. | - | - | **required** | +| body.rating | string | Rating of the show. | - | - | **required** | +| body.genre | string | The genre of anime. | - | - | **required** | +| body.studio | string | The studio of anime. | - | - | **required** | + +#### Message `trendingAnime` + +*Data required to populate trending anime* + +##### Payload + +| Name | Type | Description | Value | Constraints | Notes | +|---|---|---|---|---|---| +| (root) | object | - | - | - | **additional properties are allowed** | +| name | string | Name of the anime. | - | - | **required** | +| rating | string | Rating of the show. | - | - | **required** | +| genre | string | The genre of anime. | - | - | **required** | +| studio | string | The studio of anime. | - | - | **required** | + +> Examples of payload _(generated)_ + +```json +{ + "name": "string", + "rating": "string", + "genre": "string", + "studio": "string" +} +``` + + + +### SUB `trendingAnime` Operation + +#### `http` Channel specific information + +| Name | Type | Description | Value | Constraints | Notes | +|---|---|---|---|---|---| +| type | - | - | `"request"` | - | - | +| method | - | - | `"POST"` | - | - | +| bindingVersion | - | - | `"0.1.0"` | - | - | +| query | object | - | - | - | **additional properties are allowed** | +| query.name | string | Name of the anime. | - | - | **required** | +| query.rating | string | Rating of the show. | - | - | **required** | +| query.genre | string | The genre of anime. | - | - | **required** | +| query.studio | string | The studio of anime. | - | - | **required** | +| body | object | - | - | - | **additional properties are allowed** | +| body.name | string | Name of the anime. | - | - | **required** | +| body.rating | string | Rating of the show. | - | - | **required** | +| body.genre | string | The genre of anime. | - | - | **required** | +| body.studio | string | The studio of anime. | - | - | **required** | + +#### Message `` + +##### Payload + +| Name | Type | Description | Value | Constraints | Notes | +|---|---|---|---|---|---| +| (root) | object | - | - | - | **additional properties are allowed** | + +> Examples of payload _(generated)_ + +```json +{} +``` + + + diff --git a/examples/crypto-websockets/client/asyncapi.yaml b/examples/crypto-websockets/client/asyncapi.yaml index 458851740..b728f7a54 100644 --- a/examples/crypto-websockets/client/asyncapi.yaml +++ b/examples/crypto-websockets/client/asyncapi.yaml @@ -8,6 +8,11 @@ servers: websockets: url: ws://localhost:3000 protocol: ws + security: + - token: [] + - userPass: [] + - apiKey: [] + - cert: [] x-remoteServers: - websockets channels: @@ -31,3 +36,17 @@ components: type: number price: type: number + securitySchemes: + token: + type: http + scheme: bearer + bearerFormat: JWT + userPass: + type: userPassword + apiKey: + type: httpApiKey + name: api_key + in: header + cert: + type: apiKey + in: user \ No newline at end of file diff --git a/examples/crypto-websockets/client/auth/websockets.ts b/examples/crypto-websockets/client/auth/websockets.ts new file mode 100644 index 000000000..856a126c7 --- /dev/null +++ b/examples/crypto-websockets/client/auth/websockets.ts @@ -0,0 +1,8 @@ +export async function clientAuth({ parsedAsyncAPI, serverName }) { + return { + token: process.env.TOKEN, + userPass: { + user: "alec", password: "oviecodes" + } + } +} \ No newline at end of file diff --git a/examples/crypto-websockets/client/db.json b/examples/crypto-websockets/client/db.json index 578124390..5102a11ff 100644 --- a/examples/crypto-websockets/client/db.json +++ b/examples/crypto-websockets/client/db.json @@ -1 +1 @@ -[{"time":1663935946665,"price":130,"status":"started"},{"time":1663935947684,"price":140,"status":"intransit"},{"time":1663935948690,"price":130,"status":"intransit"},{"time":1663935949698,"price":140,"status":"intransit"},{"time":1663935950710,"price":150,"status":"intransit"},{"time":1663935951716,"price":130,"status":"intransit"},{"time":1663935952724,"price":100,"status":"intransit"},{"time":1663935953737,"price":110,"status":"intransit"},{"time":1663935954742,"price":140,"status":"intransit"}] \ No newline at end of file +[{"time":1692154441640,"price":130,"status":"started"},{"time":1692154442650,"price":140,"status":"intransit"},{"time":1692154442650,"price":140,"status":"intransit"},{"time":1692154443663,"price":180,"status":"intransit"},{"time":1692154443663,"price":180,"status":"intransit"},{"time":1692154444668,"price":180,"status":"intransit"},{"time":1692154444668,"price":180,"status":"intransit"},{"time":1692154445678,"price":160,"status":"intransit"},{"time":1692154445678,"price":160,"status":"intransit"},{"time":1692154446687,"price":120,"status":"intransit"},{"time":1692154446687,"price":120,"status":"intransit"},{"time":1692154447695,"price":110,"status":"intransit"},{"time":1692154447695,"price":110,"status":"intransit"},{"time":1692154448703,"price":130,"status":"intransit"},{"time":1692154448703,"price":130,"status":"intransit"},{"time":1692154449713,"price":130,"status":"intransit"},{"time":1692154449713,"price":130,"status":"intransit"}] \ No newline at end of file diff --git a/examples/crypto-websockets/client/docs/asyncapi.md b/examples/crypto-websockets/client/docs/asyncapi.md new file mode 100644 index 000000000..8a8935633 --- /dev/null +++ b/examples/crypto-websockets/client/docs/asyncapi.md @@ -0,0 +1,56 @@ +# asyncapicoin client 1.0.0 documentation + +This app creates a client that subscribes to the server for the price change. + + +## Table of Contents + +* [Servers](#servers) + * [websockets](#websockets-server) +* [Operations](#operations) + * [PUB /price](#pub-price-operation) + +## Servers + +### `websockets` Server + +* URL: `ws://localhost:3000` +* Protocol: `ws` + + + +## Operations + +### PUB `/price` Operation + +* Operation ID: `index` + +#### `ws` Channel specific information + +| Name | Type | Description | Value | Constraints | Notes | +|---|---|---|---|---|---| +| bindingVersion | - | - | `"0.1.0"` | - | - | + +#### Message `indexGraph` + +##### Payload + +| Name | Type | Description | Value | Constraints | Notes | +|---|---|---|---|---|---| +| (root) | object | - | - | - | **additional properties are allowed** | +| status | string | - | - | - | - | +| time | number | - | - | - | - | +| price | number | - | - | - | - | + +> Examples of payload _(generated)_ + +```json +{ + "status": "string", + "time": 0, + "price": 0 +} +``` + + + diff --git a/examples/crypto-websockets/server/asyncapi.yaml b/examples/crypto-websockets/server/asyncapi.yaml index f500b6503..4ffade405 100644 --- a/examples/crypto-websockets/server/asyncapi.yaml +++ b/examples/crypto-websockets/server/asyncapi.yaml @@ -8,6 +8,14 @@ servers: websocket: url: ws://localhost:3000 protocol: ws + security: + - token: [] + - userPass: [] + - apiKey: [] + - cert: [] + ws-websocket: + url: ws://localhost:4000 + protocol: ws channels: /price: bindings: @@ -33,4 +41,18 @@ components: time: type: number price: - type: number \ No newline at end of file + type: number + securitySchemes: + token: + type: http + scheme: bearer + bearerFormat: JWT + userPass: + type: userPassword + apiKey: + type: httpApiKey + name: api_key + in: header + cert: + type: apiKey + in: user \ No newline at end of file diff --git a/examples/crypto-websockets/server/auth/websocket.ts b/examples/crypto-websockets/server/auth/websocket.ts new file mode 100644 index 000000000..89506aa8e --- /dev/null +++ b/examples/crypto-websockets/server/auth/websocket.ts @@ -0,0 +1,22 @@ +// /* eslint-disable no-undef */ + +import axios from "axios" + +export async function serverAuth({ authProps, done }) { + await axios.get("https://jsonplaceholder.typicode.com/todos/1", { + timeout: 5000, + }) + + console.log("token", authProps.getToken()) + console.log("userpass", authProps.getUserPass()) + + done(false) +} + +export async function clientAuth({ parsedAsyncAPI, serverName }) { + return { + token: process.env.TOKEN, + username: process.env.USERNAME, + password: process.env.PASSWORD, + } +} diff --git a/examples/crypto-websockets/server/docs/asyncapi.md b/examples/crypto-websockets/server/docs/asyncapi.md new file mode 100644 index 000000000..5161cc040 --- /dev/null +++ b/examples/crypto-websockets/server/docs/asyncapi.md @@ -0,0 +1,88 @@ +# asyncapicoin server 1.0.0 documentation + +This app is a dummy server that would stream the price of a fake cryptocurrency + + +## Table of Contents + +* [Servers](#servers) + * [websocket](#websocket-server) + * [ws-websocket](#ws-websocket-server) +* [Operations](#operations) + * [SUB /price](#sub-price-operation) + +## Servers + +### `websocket` Server + +* URL: `ws://localhost:3000` +* Protocol: `ws` + + +#### Security + +##### Security Requirement 1 + +* Type: `HTTP` + * Scheme: bearer + * Bearer format: JWT + + + + +##### Security Requirement 2 + +* Type: `HTTP` + * Scheme: bearer + * Bearer format: JWT + + + + + + + +### `ws-websocket` Server + +* URL: `ws://localhost:4000` +* Protocol: `ws` + + + +## Operations + +### SUB `/price` Operation + +#### `ws` Channel specific information + +| Name | Type | Description | Value | Constraints | Notes | +|---|---|---|---|---|---| +| bindingVersion | - | - | `"0.1.0"` | - | - | +| headers | object | - | - | - | **additional properties are allowed** | +| headers.token | string | - | - | - | - | + +#### Message `indexGraph` + +*Data required for drawing index graph* + +##### Payload + +| Name | Type | Description | Value | Constraints | Notes | +|---|---|---|---|---|---| +| (root) | object | - | - | - | **additional properties are allowed** | +| status | string | - | - | - | - | +| time | number | - | - | - | - | +| price | number | - | - | - | - | + +> Examples of payload _(generated)_ + +```json +{ + "status": "string", + "time": 0, + "price": 0 +} +``` + + + diff --git a/examples/social-network/websocket-server/docs/asyncapi.md b/examples/social-network/websocket-server/docs/asyncapi.md new file mode 100644 index 000000000..ed7252d0a --- /dev/null +++ b/examples/social-network/websocket-server/docs/asyncapi.md @@ -0,0 +1,123 @@ +# The Social Network 0.1.0 documentation + + +## Table of Contents + +* [Servers](#servers) + * [websockets](#websockets-server) + * [mosquitto](#mosquitto-server) +* [Operations](#operations) + * [PUB /](#pub--operation) + * [SUB /](#sub--operation) + * [SUB post/liked](#sub-postliked-operation) + +## Servers + +### `websockets` Server + +* URL: `ws://0.0.0.0:3001` +* Protocol: `ws` + + + +### `mosquitto` Server + +* URL: `mqtt://test.mosquitto.org` +* Protocol: `mqtt` + + +#### `mqtt` Server specific information + +| Name | Type | Description | Value | Constraints | Notes | +|---|---|---|---|---|---| +| clientId | - | - | `"the-social-network"` | - | - | + + +## Operations + +### PUB `/` Operation + +* Operation ID: `onLikeDislike` +* Available only on servers: [websockets](#websockets-server) + +#### Message `likeOrDislike` + +##### Payload + +| Name | Type | Description | Value | Constraints | Notes | +|---|---|---|---|---|---| +| (root) | object | - | - | - | **additional properties are allowed** | +| type | string | Type of the message | allowed (`"like"`, `"dislike"`) | - | **required** | +| data | object | - | - | - | **additional properties are allowed** | +| data.postId | integer | The id of the post the user (dis)liked. | - | - | **required** | +| data.userId | integer | The user who clicked the Like button. | - | - | **required** | + +> Examples of payload _(generated)_ + +```json +{ + "type": "like", + "data": { + "postId": 0, + "userId": 0 + } +} +``` + + + +### SUB `/` Operation + +* Available only on servers: [websockets](#websockets-server) + +#### Message `likeCountUpdated` + +##### Payload + +| Name | Type | Description | Value | Constraints | Notes | +|---|---|---|---|---|---| +| (root) | object | - | - | - | **additional properties are allowed** | +| type | string | Type of the message | - | - | **required** | +| data | object | - | - | - | **additional properties are allowed** | +| data.postId | integer | The id of the post which likes count has been updated. | - | - | **required** | +| data.totalCount | integer | The number of likes for this post. | - | - | **required** | + +> Examples of payload _(generated)_ + +```json +{ + "type": "string", + "data": { + "postId": 0, + "totalCount": 0 + } +} +``` + + + +### SUB `post/liked` Operation + +* Available only on servers: [mosquitto](#mosquitto-server) + +#### Message `notifyPostLiked` + +##### Payload + +| Name | Type | Description | Value | Constraints | Notes | +|---|---|---|---|---|---| +| (root) | object | - | - | - | **additional properties are NOT allowed** | +| postId | integer | The id of the post that has been liked. | - | - | **required** | +| userId | integer | The user who liked the post. | - | - | **required** | + +> Examples of payload _(generated)_ + +```json +{ + "postId": 0, + "userId": 0 +} +``` + + + diff --git a/jest.config.js b/jest.config.js index 426dca83c..9682d97af 100644 --- a/jest.config.js +++ b/jest.config.js @@ -13,5 +13,9 @@ export default { }, moduleNameMapper: { '^(\\.{1,2}/.*)\\.js$': '$1', + '^nimma/legacy$': '/node_modules/nimma/dist/legacy/cjs/index.js', + '^nimma/fallbacks$': + '/node_modules/nimma/dist/legacy/cjs/fallbacks/index.js', }, -} \ No newline at end of file + transform: {}, +} diff --git a/package.json b/package.json index 48424dd09..c37207435 100644 --- a/package.json +++ b/package.json @@ -32,12 +32,6 @@ "publishConfig": { "access": "public" }, - "jest": { - "moduleNameMapper": { - "^nimma/legacy$": "/../../node_modules/nimma/dist/legacy/cjs/index.js", - "^nimma/(.*)": "/../../node_modules/nimma/dist/cjs/$1" - } - }, "license": "Apache-2.0", "dependencies": { "@asyncapi/generator": "^1.9.18", diff --git a/src/adapters/http/client.ts b/src/adapters/http/client.ts index fe1aa1f24..efb53b4c2 100644 --- a/src/adapters/http/client.ts +++ b/src/adapters/http/client.ts @@ -1,8 +1,10 @@ -import Adapter from '../../lib/adapter.js' -import GleeMessage from '../../lib/message.js' import got from 'got' -import { HttpAuthConfig, HttpAdapterConfig } from '../../lib/index.js' import http from 'http' +import Adapter from '../../lib/adapter.js' +import GleeMessage from '../../lib/message.js' +import { clientAuthConfig } from '../../lib/userAuth.js' +import GleeAuth from '../../lib/wsHttpAuth.js' + class HttpClientAdapter extends Adapter { name(): string { return 'HTTP client' @@ -18,10 +20,8 @@ class HttpClientAdapter extends Adapter { } async send(message: GleeMessage): Promise { - const headers = {} - const config: HttpAdapterConfig = await this.resolveProtocolConfig('http') - const auth: HttpAuthConfig = await this.getAuthConfig(config?.client?.auth) - headers['Authentication'] = auth?.token + let headers = {} + const authConfig = await clientAuthConfig(this.serverName) const serverUrl = this.serverUrlExpanded for (const channelName of this.channelNames) { const channelInfo = this.parsedAsyncAPI.channel(channelName) @@ -31,15 +31,34 @@ class HttpClientAdapter extends Adapter { !channelServers.length || channelServers.includes(message.serverName) if (httpChannelBinding && isChannelServers) { const method = httpChannelBinding.method - const url = `${serverUrl}/${channelName}` + let url = `${serverUrl}/${channelName}` + const gleeAuth = new GleeAuth( + this.AsyncAPIServer, + this.parsedAsyncAPI, + this.serverName, + authConfig + ) const body: any = message.payload - const query: { [key: string]: string } | { [key: string]: string[] } = + let query: { [key: string]: string } | { [key: string]: string[] } = message.query + + if (authConfig) { + const modedAuth = await gleeAuth.processClientAuth( + url, + headers, + query + ) + headers = modedAuth.headers + url = modedAuth.url.href + query = modedAuth.query + } + got({ method, url, json: body, searchParams: JSON.parse(JSON.stringify(query)), + headers, }) .then((res) => { const msg = this.createMessage(channelName, res.body) diff --git a/src/adapters/http/server.ts b/src/adapters/http/server.ts index d207d2dad..be9c27975 100644 --- a/src/adapters/http/server.ts +++ b/src/adapters/http/server.ts @@ -4,6 +4,7 @@ import http from 'http' import { validateData } from '../../lib/util.js' import GleeError from '../../errors/glee-error.js' import * as url from 'url' +import GleeAuth from '../../lib/wsHttpAuth.js' class HttpAdapter extends Adapter { private httpResponses = new Map() @@ -30,14 +31,64 @@ class HttpAdapter extends Adapter { const optionsPort = httpOptions?.port const port = optionsPort || asyncapiServerPort - httpServer.on('request', (req, res) => { + httpServer.on('request', async (req, res) => { res.setHeader('Content-Type', 'application/json') + const bodyBuffer = [] let body: object req.on('data', (chunk) => { bodyBuffer.push(chunk) }) - req.on('end', () => { + + function done() { + let resolveFunc, rejectFunc + const promise = new Promise((resolve, reject) => { + resolveFunc = resolve + rejectFunc = reject + }) + return { + promise, + done: (val: boolean, code = 401, message = 'Unauthorized') => { + if (val) { + resolveFunc(true) + } else { + rejectFunc({ code, message }) + } + }, + } + } + + const gleeAuth = new GleeAuth( + this.AsyncAPIServer, + this.parsedAsyncAPI, + this.serverName, + req.headers + ) + + const { promise, done: callback } = done() + + if (gleeAuth.checkAuthPresense()) { + this.emit('auth', { + authProps: gleeAuth.getServerAuthProps( + req.headers, + url.parse(req.url, true).query + ), + server: this.serverName, + done: callback, + doc: this.AsyncAPIServer, + }) + } + + req.on('end', async () => { + try { + if (gleeAuth.checkAuthPresense()) await promise + } catch (e) { + res.statusCode = e.code + res.end() + this.emit('error', new Error(`${e.code} ${e.message}`)) + return + } + body = JSON.parse(Buffer.concat(bodyBuffer).toString()) this.httpResponses.set(this.serverName, res) let { pathname } = new URL(req.url, serverUrl) diff --git a/src/adapters/ws/client.ts b/src/adapters/ws/client.ts index 6dca108ee..a6949a5fd 100644 --- a/src/adapters/ws/client.ts +++ b/src/adapters/ws/client.ts @@ -2,7 +2,8 @@ import Adapter from '../../lib/adapter.js' import GleeMessage from '../../lib/message.js' import ws from 'ws' -import { WsAuthConfig, WebsocketAdapterConfig } from '../../lib/index.js' +import { clientAuthConfig } from '../../lib/userAuth.js' +import GleeAuth from '../../lib/wsHttpAuth.js' interface Client { channel: string @@ -29,14 +30,20 @@ class WsClientAdapter extends Adapter { const channelsOnThisServer = this.getWsChannels() for (const channel of channelsOnThisServer) { - const headers = {} - const wsOptions: WebsocketAdapterConfig = - await this.resolveProtocolConfig('ws') - const auth: WsAuthConfig = await this.getAuthConfig(wsOptions?.client?.auth) - headers['Authentication'] = `bearer ${auth?.token}` - - const url = new URL(this.AsyncAPIServer.url() + channel) - + let headers = {} + const authConfig = await clientAuthConfig(this.serverName) + const gleeAuth = new GleeAuth( + this.AsyncAPIServer, + this.parsedAsyncAPI, + this.serverName, + authConfig + ) + let url = new URL(this.AsyncAPIServer.url() + channel) + if (authConfig) { + const modedAuth = await gleeAuth.processClientAuth(url, headers, {}) + headers = modedAuth.headers + url = modedAuth.url + } this.clients.push({ channel, client: new ws(url, { headers }), diff --git a/src/adapters/ws/server.ts b/src/adapters/ws/server.ts index 4591b74f0..ea46db276 100644 --- a/src/adapters/ws/server.ts +++ b/src/adapters/ws/server.ts @@ -5,6 +5,7 @@ import Adapter from '../../lib/adapter.js' import GleeConnection from '../../lib/connection.js' import GleeMessage from '../../lib/message.js' import GleeError from '../../errors/glee-error.js' +import GleeAuth from '../../lib/wsHttpAuth.js' type QueryData = { searchParams: URLSearchParams @@ -141,7 +142,7 @@ class WebSocketsAdapter extends Adapter { } } - private checkBindings(socket, bindingOpts) { + private async checkBindings(socket, bindingOpts) { const { wsChannelBinding, request, searchParams } = bindingOpts const { query, headers } = wsChannelBinding @@ -162,6 +163,7 @@ class WebSocketsAdapter extends Adapter { request, headers, }) + if (!isValid) { this.emitGleeError(socket, { humanReadableError, errors }) return false @@ -171,18 +173,55 @@ class WebSocketsAdapter extends Adapter { return true } + private wrapCallbackDecorator(cb) { + return function done(val: boolean, code = 401, message = 'Unauthorized') { + cb(val, code, message) + if (val === false) { + const err = new Error(`${code} ${message}`) + this.emit('error', err) + } + } + } + + private verifyClientFunc(gleeAuth, info, cb) { + const authProps = gleeAuth.getServerAuthProps(info.req.headers, {}) + const done = this.wrapCallbackDecorator(cb).bind(this) + this.emit('auth', { + authProps, + server: this.serverName, + done, + doc: this.AsyncAPIServer, + }) + } + async _connect(): Promise { const { config, serverUrl, wsHttpServer, optionsPort, port } = await this.initializeConstants() this.portChecks({ port, config, optionsPort, wsHttpServer }) + const gleeAuth = new GleeAuth( + this.AsyncAPIServer, + this.parsedAsyncAPI, + this.serverName + ) + const servers = new Map() this.channelNames.forEach((channelName) => { - servers.set(channelName, new WebSocket.Server({ noServer: true })) + servers.set( + channelName, + new WebSocket.Server({ + noServer: true, + verifyClient: gleeAuth.checkAuthPresense() + ? (info, cb) => { + this.verifyClientFunc(gleeAuth, info, cb) + } + : null, + }) + ) }) - wsHttpServer.on('upgrade', (request, socket, head) => { + wsHttpServer.on('upgrade', async (request, socket, head) => { let { pathname } = new URL(request.url, `ws://${request.headers.host}`) pathname = this.pathnameChecks(socket, pathname, { serverUrl, servers }) @@ -191,12 +230,13 @@ class WebSocketsAdapter extends Adapter { request.url, `ws://${request.headers.host}` ) + const wsChannelBinding = this.parsedAsyncAPI .channel(pathname) .binding('ws') if (wsChannelBinding) { - const correctBindings = this.checkBindings(socket, { + const correctBindings = await this.checkBindings(socket, { wsChannelBinding, request, searchParams, @@ -232,12 +272,14 @@ class WebSocketsAdapter extends Adapter { connection.getRaw().send(message.payload) }) } else { - if (!message.connection) - {throw new Error( + if (!message.connection) { + throw new Error( 'There is no WebSocket connection to send the message yet.' - )} - if (!(message.connection instanceof GleeConnection)) - {throw new Error('Connection object is not of GleeConnection type.')} + ) + } + if (!(message.connection instanceof GleeConnection)) { + throw new Error('Connection object is not of GleeConnection type.') + } message.connection.getRaw().send(message.payload) } } diff --git a/src/index.ts b/src/index.ts index 7dcbded8a..73ffd8d82 100755 --- a/src/index.ts +++ b/src/index.ts @@ -13,6 +13,10 @@ import { register as registerFunctions, trigger as triggerFunction, } from './lib/functions.js' +import { + register as registerAuth, + triggerAuth as runAuth, +} from './lib/userAuth.js' import buffer2string from './middlewares/buffer2string.js' import string2json from './middlewares/string2json.js' import json2string from './middlewares/json2string.js' @@ -25,15 +29,20 @@ import validateConnection from './middlewares/validateConnection.js' import { initializeConfigs } from './lib/configs.js' import { getParsedAsyncAPI } from './lib/asyncapiFile.js' import { getSelectedServerNames } from './lib/servers.js' -import { EnrichedEvent } from './lib/adapter.js' +import { EnrichedEvent, AuthEvent } from './lib/adapter.js' import { ClusterEvent } from './lib/cluster.js' dotenvExpand(dotenv.config()) export default async function GleeAppInitializer() { const config = await initializeConfigs() - const { GLEE_DIR, GLEE_PROJECT_DIR, GLEE_LIFECYCLE_DIR, GLEE_FUNCTIONS_DIR } = - config + const { + GLEE_DIR, + GLEE_PROJECT_DIR, + GLEE_LIFECYCLE_DIR, + GLEE_FUNCTIONS_DIR, + GLEE_AUTH_DIR, + } = config logWelcome({ dev: process.env.NODE_ENV === 'development', @@ -47,6 +56,7 @@ export default async function GleeAppInitializer() { await registerFunctions(GLEE_FUNCTIONS_DIR) await registerLifecycleEvents(GLEE_LIFECYCLE_DIR) + await registerAuth(GLEE_AUTH_DIR) const parsedAsyncAPI = await getParsedAsyncAPI() const channelNames = parsedAsyncAPI.channelNames() @@ -57,6 +67,7 @@ export default async function GleeAppInitializer() { app.use(existsInAsyncAPI(parsedAsyncAPI)) app.useOutbound(existsInAsyncAPI(parsedAsyncAPI)) + app.useOutbound(validateConnection) app.use(buffer2string) app.use(string2json) @@ -99,6 +110,23 @@ export default async function GleeAppInitializer() { } }) + app.on('adapter:auth', async (e: AuthEvent) => { + logLineWithIcon( + ':zap:', + `Running authentication on server ${e.serverName}.`, + { + highlightedWords: [e.serverName], + } + ) + await runAuth({ + glee: app, + serverName: e.serverName, + authProps: e.authProps, + done: e.done, + doc: e.doc, + }) + }) + app.on('adapter:connect', async (e: EnrichedEvent) => { logLineWithIcon(':zap:', `Connected to server ${e.serverName}.`, { highlightedWords: [e.serverName], @@ -138,7 +166,9 @@ export default async function GleeAppInitializer() { app.on('adapter:server:ready', async (e: EnrichedEvent) => { logLineWithIcon( ':zap:', - `Server ${e.serverName} is ready to accept connections on ${e.server.url()}.`, + `Server ${ + e.serverName + } is ready to accept connections on ${e.server.url()}.`, { highlightedWords: [e.serverName], } diff --git a/src/lib/adapter.ts b/src/lib/adapter.ts index c8bac0482..2119d53df 100644 --- a/src/lib/adapter.ts +++ b/src/lib/adapter.ts @@ -6,6 +6,7 @@ import GleeConnection from './connection.js' import Glee from './glee.js' import GleeMessage from './message.js' import { resolveFunctions } from './util.js' +import { AuthProps } from './index.js' export type EnrichedEvent = { connection?: GleeConnection @@ -13,6 +14,13 @@ export type EnrichedEvent = { server: Server } +export type AuthEvent = { + serverName: string + authProps: AuthProps + done: any + doc: any +} + class GleeAdapter extends EventEmitter { private _glee: Glee private _serverName: string @@ -49,8 +57,9 @@ class GleeAdapter extends EventEmitter { const uriTemplateValues = new Map() process.env.GLEE_SERVER_VARIABLES?.split(',').forEach((t) => { const [localServerName, variable, value] = t.split(':') - if (localServerName === this._serverName) - {uriTemplateValues.set(variable, value)} + if (localServerName === this._serverName) { + uriTemplateValues.set(variable, value) + } }) this._serverUrlExpanded = uriTemplates(this._AsyncAPIServer.url()).fill( Object.fromEntries(uriTemplateValues.entries()) @@ -81,6 +90,18 @@ class GleeAdapter extends EventEmitter { } } + function enrichAuthEvent(ev): AuthEvent { + return { + ...ev, + ...{ + serverName, + authProps: ev.authProps, + callback: ev.callback, + doc: ev.doc, + }, + } + } + function createConnection(ev: { channels?: string[] channel?: string @@ -98,6 +119,10 @@ class GleeAdapter extends EventEmitter { }) } + this.on('auth', (ev) => { + this._glee.emit('adapter:auth', enrichAuthEvent(ev)) + }) + this.on('connect', (ev) => { const conn = createConnection(ev) this._connections.push(conn) @@ -226,7 +251,10 @@ class GleeAdapter extends EventEmitter { * * @param {GleeMessage} message The message to send. */ - async send(message: GleeMessage): Promise { // eslint-disable-line @typescript-eslint/no-unused-vars + + async send( + message: GleeMessage /* eslint-disable-line @typescript-eslint/no-unused-vars */ + ): Promise { throw new Error('Method `send` is not implemented.') } } diff --git a/src/lib/configs.ts b/src/lib/configs.ts index 438582581..0e594245c 100644 --- a/src/lib/configs.ts +++ b/src/lib/configs.ts @@ -11,6 +11,7 @@ let GLEE_DIR: string let GLEE_PROJECT_DIR: string let GLEE_LIFECYCLE_DIR: string let GLEE_FUNCTIONS_DIR: string +let GLEE_AUTH_DIR: string let GLEE_CONFIG_FILE_PATH: string let GLEE_CONFIG_FILE_PATH_JS: string let GLEE_CONFIG_FILE_PATH_TS: string @@ -30,6 +31,7 @@ export async function initializeConfigs( GLEE_DIR, config.functionsDir || 'functions' ) + GLEE_AUTH_DIR = path.resolve(GLEE_DIR, config.functionsDir || 'auth') GLEE_CONFIG_FILE_PATH_TS = path.resolve(GLEE_DIR, 'glee.config.ts') GLEE_CONFIG_FILE_PATH_JS = path.resolve(GLEE_DIR, 'glee.config.js') @@ -80,13 +82,15 @@ async function loadConfigsFromFile() { let { default: projectConfigs } = await import( pathToFileURL(GLEE_CONFIG_FILE_PATH).href ) - if (typeof projectConfigs === 'function') - {projectConfigs = await projectConfigs()} + if (typeof projectConfigs === 'function') { + projectConfigs = await projectConfigs() + } if (!projectConfigs) return GLEE_DIR = projectConfigs.glee?.gleeDir || GLEE_DIR GLEE_LIFECYCLE_DIR = projectConfigs.glee?.lifecycleDir ?? GLEE_LIFECYCLE_DIR GLEE_FUNCTIONS_DIR = projectConfigs.glee?.functionsDir ?? GLEE_FUNCTIONS_DIR + GLEE_AUTH_DIR = projectConfigs.glee?.authDir ?? GLEE_AUTH_DIR ASYNCAPI_FILE_PATH = projectConfigs.glee?.asyncapiFilePath ?? ASYNCAPI_FILE_PATH return projectConfigs @@ -120,6 +124,7 @@ export function getConfigs(): { [key: string]: string } { GLEE_LIFECYCLE_DIR, GLEE_FUNCTIONS_DIR, GLEE_CONFIG_FILE_PATH, + GLEE_AUTH_DIR, ASYNCAPI_FILE_PATH, } } diff --git a/src/lib/glee.ts b/src/lib/glee.ts index c144248e9..514881f88 100644 --- a/src/lib/glee.ts +++ b/src/lib/glee.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ import EventEmitter from 'events' import async from 'async' import Debug from 'debug' @@ -100,7 +101,10 @@ export default class Glee extends EventEmitter { */ use(...middlewares: GenericMiddleware[]): void use(channel: string, ...middlewares: GenericMiddleware[]): void - use(channel: string | GenericMiddleware, ...middlewares: GenericMiddleware[]): void { // eslint-disable-line @typescript-eslint/no-unused-vars + use( + channel: string | GenericMiddleware, + ...middlewares: GenericMiddleware[] + ): void { this._router.use(...arguments) // eslint-disable-line prefer-rest-params } @@ -115,7 +119,6 @@ export default class Glee extends EventEmitter { channel: string | GenericMiddleware, ...middlewares: GenericMiddleware[] ): void { - // eslint-disable-line @typescript-eslint/no-unused-vars this._router.useOutbound(...arguments) // eslint-disable-line prefer-rest-params } diff --git a/src/lib/index.d.ts b/src/lib/index.d.ts index 4b3d3c5a1..5abe5c03d 100644 --- a/src/lib/index.d.ts +++ b/src/lib/index.d.ts @@ -25,12 +25,30 @@ export interface MqttAuthConfig { export interface WsAuthConfig { token?: string + username?: string + password?: string } export interface HttpAuthConfig { token?: string + username?: string + password?: string +} + +export type AuthProps = { + getToken: () => string + getUserPass: () => { + username: string + password: string + } + getCert: () => string + getOauthToken: () => string + getHttpAPIKeys: (name: string) => string + getAPIKeys: () => string } +export type WsHttpAuth = WsAuthConfig | HttpAuthConfig + export interface KafkaAuthConfig { key?: string cert?: string @@ -109,6 +127,14 @@ export type GleeFunctionEvent = { channel?: string } +export type GleeAuthFunctionEvent = { + glee: Glee + authProps: AuthProps + done: any + serverName: string + doc: any +} + export type GleeFunctionReturnSend = { payload?: any query?: QueryParam @@ -123,3 +149,7 @@ export type GleeFunctionReturnBroadcast = GleeFunctionReturnSend export type GleeFunction = ( event: GleeFunctionEvent ) => Promise + +export type GleeAuthFunction = ( + event: GleeAuthFunctionEvent +) => Promise diff --git a/src/lib/userAuth.ts b/src/lib/userAuth.ts new file mode 100644 index 000000000..25d57803c --- /dev/null +++ b/src/lib/userAuth.ts @@ -0,0 +1,79 @@ +import { basename, extname } from 'path' +import { stat } from 'fs/promises' +import walkdir from 'walkdir' +import { logWarningMessage } from './logger.js' +import { GleeAuthFunction, GleeAuthFunctionEvent } from './index.js' +import { pathToFileURL } from 'url' + +interface AuthFunctionInfo { + clientAuth?: GleeAuthFunction + serverAuth?: GleeAuthFunction +} + +export const authFunctions: Map = new Map() + +export async function register(dir: string) { + try { + const statsDir = await stat(dir) + if (!statsDir.isDirectory()) return + } catch (e) { + if (e.code === 'ENOENT') return + throw e + } + + //get serverAuth and ClientAuth + try { + const files = await walkdir.async(dir, { return_object: true }) + return await Promise.all( + Object.keys(files).map(async (filePath) => { + try { + const serverName = basename(filePath, extname(filePath)) + const { clientAuth, serverAuth } = await import( + pathToFileURL(filePath).href + ) + authFunctions.set(serverName, { + clientAuth, + serverAuth, + }) + } catch (e) { + console.error(e) + } + }) + ) + } catch (e) { + console.error(e) + } +} +export async function triggerAuth(params: GleeAuthFunctionEvent) { + const { serverName, done } = params + + try { + const auth = authFunctions.get(serverName) + if (!auth) { + logWarningMessage( + `Missing Authentication function file. Cannot find ${serverName}.ts or ${serverName}.js`, + { + highlightedWords: [serverName], + } + ) + done(false, 422, 'Cannot find authentication file') + return + } + //run serverAuth function with passed parameters + await auth.serverAuth(params) + return + } catch (err) { + if (err.code === 'ERR_MODULE_NOT_FOUND') { + logWarningMessage(`Missing function file ${serverName}.`, { + highlightedWords: [serverName], + }) + } else { + throw err + } + } +} + +export async function clientAuthConfig(serverName: string) { + //get client credentials + return authFunctions.get(serverName)?.clientAuth +} diff --git a/src/lib/wsHttpAuth.ts b/src/lib/wsHttpAuth.ts new file mode 100644 index 000000000..60e438f73 --- /dev/null +++ b/src/lib/wsHttpAuth.ts @@ -0,0 +1,180 @@ +import { AsyncAPIDocument, SecurityScheme, Server } from '@asyncapi/parser' +import { resolveFunctions } from './util.js' +import { EventEmitter } from 'events' +import { HttpAuthConfig, WsAuthConfig, AuthProps } from './index.js' + +class GleeAuth extends EventEmitter { + private secReqs: { [key: string]: SecurityScheme }[] + private parsedAsyncAPI: AsyncAPIDocument + private serverName: string + private AsyncAPIServer: Server + private authConfig: WsAuthConfig | HttpAuthConfig + private auth: { [key: string]: string } | { [key: string]: string[] } + + /** + * Instantiates authentication. + */ + constructor( + AsyncAPIServer: Server, + parsedAsyncAPI: AsyncAPIDocument, + serverName: string, + authConfig? + ) { + super() + this.secReqs = [] + this.parsedAsyncAPI = parsedAsyncAPI + this.serverName = serverName + this.AsyncAPIServer = AsyncAPIServer + this.authConfig = authConfig + } + + checkClientAuthConfig() { + this.secReqs = (this.AsyncAPIServer.security() || []).map((sec) => { + const secName = Object.keys(sec.json())[0] + return { + [secName]: this.parsedAsyncAPI.components().securityScheme(secName), + } + }) + + const authKeys = Object.keys(this.auth) + const secNames = this.secReqs.map((el) => Object.keys(el)[0]) + + authKeys.forEach((el) => { + const allowed = secNames.includes(el) + if (!allowed) { + const err = new Error( + `${el} securityScheme is not defined in your asyncapi.yaml config` + ) + this.emit('error', err) + } + }) + + return authKeys + + //checkClientUnimplementedSecScheme() + //raise a warning about any unimplemented securityScheme + } + + async getAuthConfig(auth) { + if (!auth) return + if (typeof auth !== 'function') { + await resolveFunctions(auth) + return auth + } + + return await auth({ + serverName: this.serverName, + parsedAsyncAPI: this.parsedAsyncAPI, + }) + } + + formClientAuth(authKeys, { url, headers, query }) { + if (!authKeys) return { url, headers } + authKeys.map((authKey) => { + const scheme = this.secReqs.find((sec) => Object.keys(sec) == authKey) + const currentScheme = scheme[String(authKey)].scheme() + const currentType = scheme[String(authKey)].type() + if (currentScheme == 'bearer') { + headers.authentication = `bearer ${this.auth[String(authKey)]}` + return + } + if (currentType == 'userPassword' || currentType == 'apiKey') { + url = this.userPassApiKeyLogic(url, authKey) + return + } + if (currentType == 'oauth2') { + headers.oauthToken = this.auth[String(authKey)] + } + if (currentType == 'httpApiKey') { + const conf = this.httpApiKeyLogic(scheme, headers, query, authKey) + headers = conf.headers + query = conf.query + } + }) + return { url, headers, query } + } + + private userPassApiKeyLogic(url, authKey) { + const password = this.auth[String(authKey)]['password'] + const username = this.auth[String(authKey)]['user'] + + if (typeof url == 'object') { + url.password = password + url.username = username + return url + } + + const myURL = new URL(url) + myURL.password = password + myURL.username = username + return myURL + } + + private httpApiKeyLogic(scheme, headers, query, authKey) { + const loc = scheme[String(authKey)].json('in') + if (loc == 'header') { + headers[scheme[String(authKey)].json('name')] = this.auth[String(authKey)] + } else if (loc == 'query') { + query[scheme[String(authKey)].json('name')] = this.auth[String(authKey)] + } + + return { headers, query } + } + + // getServerAuthReq() {} + + getServerAuthProps(headers, query) { + const authProps: AuthProps = { + getToken: () => { + return headers.authentication + }, + getUserPass: () => { + const buf = headers.authorization + ? Buffer.from(headers.authorization?.split(' ')[1], 'base64') + : undefined + + if (!buf) return + + const [username, password] = buf.toString().split(':') + return { + username, + password, + } + }, + getCert: () => { + return headers.cert + }, + getOauthToken: () => { + return headers.oauthtoken + }, + getHttpAPIKeys: (name: string) => { + return headers[String(name)] ?? query[String(name)] + }, + getAPIKeys: () => { + return `keys` + }, + } + + return authProps + } + + async processClientAuth(url, headers, query) { + this.auth = await this.getAuthConfig(this.authConfig) + const authKeys = this.checkClientAuthConfig() + if (!authKeys) return + return this.formClientAuth(authKeys, { url, headers, query }) + } + + checkAuthPresense(): boolean { + return ( + this.AsyncAPIServer.security() && + Object.keys(this.AsyncAPIServer.security()).length > 0 + ) + } + + // checkClientUnimplementedSecScheme() {} + + // getSchemes(type) {} +} + +export default GleeAuth diff --git a/test/lib/adapter.test.ts b/test/lib/adapter.test.ts index 4130b1fa0..d901fca7a 100644 --- a/test/lib/adapter.test.ts +++ b/test/lib/adapter.test.ts @@ -1,12 +1,12 @@ import 'jest-extended' import AsyncAPIDocument from '@asyncapi/parser/lib/models/asyncapi' -import {Server} from '@asyncapi/parser' +import { Server } from '@asyncapi/parser' import GleeConnection from '../../src/lib/connection.js' import Glee from '../../src/lib/glee.js' import GleeMessage from '../../src/lib/message.js' import GleeAdapter from '../../src/lib/adapter.js' import { MiddlewareCallback } from '../../src/middlewares/index.d' -import {jest} from '@jest/globals' +import { jest } from '@jest/globals' const TEST_SERVER_NAME = 'test' const ANOTHER_TEST_SERVER_NAME = 'another' @@ -23,9 +23,9 @@ const TEST_ASYNCAPI_DOCUMENT = new AsyncAPIDocument({ protocol: 'ws', variables: { port: { - default: '7000' - } - } + default: '7000', + }, + }, }, }, channels: { @@ -34,18 +34,25 @@ const TEST_ASYNCAPI_DOCUMENT = new AsyncAPIDocument({ message: { payload: { type: 'string', - } - } - } - } - } + }, + }, + }, + }, + }, }) const TEST_SERVER: Server = TEST_ASYNCAPI_DOCUMENT.server(TEST_SERVER_NAME) -const ANOTHER_TEST_SERVER: Server = TEST_ASYNCAPI_DOCUMENT.server(ANOTHER_TEST_SERVER_NAME) +const ANOTHER_TEST_SERVER: Server = TEST_ASYNCAPI_DOCUMENT.server( + ANOTHER_TEST_SERVER_NAME +) const RAW_CONN = { fake: 'conn' } class TEST_ADAPTER extends GleeAdapter { async connect() { - this.emit('connect', { name: 'TEST_ADAPTER', adapter: this, connection: RAW_CONN, channels: [] }) + this.emit('connect', { + name: 'TEST_ADAPTER', + adapter: this, + connection: RAW_CONN, + channels: [], + }) } } class ANOTHER_TEST_ADAPTER extends GleeAdapter {} @@ -58,54 +65,108 @@ const fakeConnection = new GleeConnection({ parsedAsyncAPI: TEST_ASYNCAPI_DOCUMENT, }) +const initializeAdapter = () => { + const app = new Glee() + const adapter = new GleeAdapter( + app, + TEST_SERVER_NAME, + TEST_SERVER, + TEST_ASYNCAPI_DOCUMENT + ) + app.on('adapter:server:connection:open', (ev) => { + expect(ev.serverName).toStrictEqual(TEST_SERVER_NAME) + expect(ev.server).toStrictEqual(TEST_SERVER) + expect(ev.connection).toBeInstanceOf(GleeConnection) + expect(ev.connection.AsyncAPIServer).toStrictEqual(TEST_SERVER) + expect(ev.connection.rawConnection).toStrictEqual(RAW_CONN) + expect(ev.connection.channels).toStrictEqual(['fake/channel']) + expect(ev.connection.serverName).toStrictEqual(TEST_SERVER_NAME) + expect(ev.connection.parsedAsyncAPI).toStrictEqual(TEST_ASYNCAPI_DOCUMENT) + }) + + return adapter +} + describe('adapter', () => { describe('glee', () => { it('returns the glee app passed on constructor', async () => { const app = new Glee() - const adapter = new GleeAdapter(app, TEST_SERVER_NAME, TEST_SERVER, TEST_ASYNCAPI_DOCUMENT) + const adapter = new GleeAdapter( + app, + TEST_SERVER_NAME, + TEST_SERVER, + TEST_ASYNCAPI_DOCUMENT + ) expect(adapter.glee).toStrictEqual(app) }) }) - + describe('serverName', () => { it('returns the server name passed on constructor', async () => { const app = new Glee() - const adapter = new GleeAdapter(app, TEST_SERVER_NAME, TEST_SERVER, TEST_ASYNCAPI_DOCUMENT) + const adapter = new GleeAdapter( + app, + TEST_SERVER_NAME, + TEST_SERVER, + TEST_ASYNCAPI_DOCUMENT + ) expect(adapter.serverName).toStrictEqual(TEST_SERVER_NAME) }) }) - + describe('AsyncAPIServer', () => { it('returns the AsyncAPI server object passed on constructor', async () => { const app = new Glee() - const adapter = new GleeAdapter(app, TEST_SERVER_NAME, TEST_SERVER, TEST_ASYNCAPI_DOCUMENT) + const adapter = new GleeAdapter( + app, + TEST_SERVER_NAME, + TEST_SERVER, + TEST_ASYNCAPI_DOCUMENT + ) expect(adapter.AsyncAPIServer).toStrictEqual(TEST_SERVER) }) }) - + describe('parsedAsyncAPI', () => { it('returns the AsyncAPI document object passed on constructor', async () => { const app = new Glee() - const adapter = new GleeAdapter(app, TEST_SERVER_NAME, TEST_SERVER, TEST_ASYNCAPI_DOCUMENT) + const adapter = new GleeAdapter( + app, + TEST_SERVER_NAME, + TEST_SERVER, + TEST_ASYNCAPI_DOCUMENT + ) expect(adapter.parsedAsyncAPI).toStrictEqual(TEST_ASYNCAPI_DOCUMENT) }) }) - + describe('channelNames', () => { it('returns the list of associated channel names', async () => { const app = new Glee() - const adapter = new GleeAdapter(app, TEST_SERVER_NAME, TEST_SERVER, TEST_ASYNCAPI_DOCUMENT) - expect(adapter.channelNames).toStrictEqual(TEST_ASYNCAPI_DOCUMENT.channelNames()) + const adapter = new GleeAdapter( + app, + TEST_SERVER_NAME, + TEST_SERVER, + TEST_ASYNCAPI_DOCUMENT + ) + expect(adapter.channelNames).toStrictEqual( + TEST_ASYNCAPI_DOCUMENT.channelNames() + ) }) }) - + describe('connections', () => { it('returns an empty array when the adapter is just initialized', async () => { const app = new Glee() - const adapter = new GleeAdapter(app, TEST_SERVER_NAME, TEST_SERVER, TEST_ASYNCAPI_DOCUMENT) + const adapter = new GleeAdapter( + app, + TEST_SERVER_NAME, + TEST_SERVER, + TEST_ASYNCAPI_DOCUMENT + ) expect(adapter.connections).toStrictEqual([]) }) - + it('returns a array with the associated connections', async () => { const app = new Glee() app.addAdapter(TEST_ADAPTER, { @@ -114,11 +175,13 @@ describe('adapter', () => { parsedAsyncAPI: TEST_ASYNCAPI_DOCUMENT, }) await app.connect() - + expect(app.adapters.length).toStrictEqual(1) expect(app.adapters[0].instance).toBeTruthy() expect(app.adapters[0].instance.connections.length).toStrictEqual(1) - expect(app.adapters[0].instance.connections[0].rawConnection).toStrictEqual(RAW_CONN) + expect( + app.adapters[0].instance.connections[0].rawConnection + ).toStrictEqual(RAW_CONN) }) }) @@ -129,25 +192,32 @@ describe('adapter', () => { jest.resetModules() // Most important - it clears the cache process.env = { ...OLD_ENV, - GLEE_SERVER_VARIABLES: 'another:port:8000' + GLEE_SERVER_VARIABLES: 'another:port:8000', } }) afterAll(() => { process.env = OLD_ENV // Restore old environment }) - + it('returns the server URL with variables expanded', async () => { const app = new Glee() - const adapter = new GleeAdapter(app, ANOTHER_TEST_SERVER_NAME, ANOTHER_TEST_SERVER, TEST_ASYNCAPI_DOCUMENT) - expect(adapter.serverUrlExpanded).toStrictEqual('ws://fake-url-with-vars:8000') + const adapter = new GleeAdapter( + app, + ANOTHER_TEST_SERVER_NAME, + ANOTHER_TEST_SERVER, + TEST_ASYNCAPI_DOCUMENT + ) + expect(adapter.serverUrlExpanded).toStrictEqual( + 'ws://fake-url-with-vars:8000' + ) }) }) - + describe('on("message")', () => { it('injects the message on the Glee app', async () => { const msg = new GleeMessage({ - payload: 'test' + payload: 'test', }) const app = new Glee() app.addAdapter(TEST_ADAPTER, { @@ -163,103 +233,90 @@ describe('adapter', () => { expect(app.adapters.length).toStrictEqual(1) expect(app.adapters[0].instance).toBeTruthy() - app.adapters[0].instance.emit('message', msg, RAW_CONN) + app.adapters[0].instance.emit('message', msg, RAW_CONN) }) }) - + describe('on("server:ready")', () => { it('notifies the Glee app', async () => { - const app = new Glee() - const adapter = new GleeAdapter(app, TEST_SERVER_NAME, TEST_SERVER, TEST_ASYNCAPI_DOCUMENT) - app.on('adapter:server:ready', (ev) => { - expect(ev).toStrictEqual({ - fake: 'object', - serverName: TEST_SERVER_NAME, - server: TEST_SERVER, - }) - }) + const adapter = initializeAdapter() adapter.emit('server:ready', { fake: 'object' }) }) }) - + describe('on("server:connection:open")', () => { it('notifies the Glee app', async () => { - const app = new Glee() - const adapter = new GleeAdapter(app, TEST_SERVER_NAME, TEST_SERVER, TEST_ASYNCAPI_DOCUMENT) - app.on('adapter:server:connection:open', (ev) => { - expect(ev.serverName).toStrictEqual(TEST_SERVER_NAME) - expect(ev.server).toStrictEqual(TEST_SERVER) - expect(ev.connection).toBeInstanceOf(GleeConnection) - expect(ev.connection.AsyncAPIServer).toStrictEqual(TEST_SERVER) - expect(ev.connection.rawConnection).toStrictEqual(RAW_CONN) - expect(ev.connection.channels).toStrictEqual(['fake/channel']) - expect(ev.connection.serverName).toStrictEqual(TEST_SERVER_NAME) - expect(ev.connection.parsedAsyncAPI).toStrictEqual(TEST_ASYNCAPI_DOCUMENT) + const adapter = initializeAdapter() + adapter.emit('server:connection:open', { + channels: ['fake/channel'], + connection: RAW_CONN, }) - adapter.emit('server:connection:open', { channels: ['fake/channel'], connection: RAW_CONN }) }) }) - + describe('on("close")', () => { it('notifies the Glee app', async () => { - const app = new Glee() - const adapter = new GleeAdapter(app, TEST_SERVER_NAME, TEST_SERVER, TEST_ASYNCAPI_DOCUMENT) - app.on('adapter:close', (ev) => { - expect(ev.serverName).toStrictEqual(TEST_SERVER_NAME) - expect(ev.server).toStrictEqual(TEST_SERVER) - expect(ev.connection).toBeInstanceOf(GleeConnection) - expect(ev.connection.AsyncAPIServer).toStrictEqual(TEST_SERVER) - expect(ev.connection.rawConnection).toStrictEqual(RAW_CONN) - expect(ev.connection.channels).toStrictEqual(['fake/channel']) - expect(ev.connection.serverName).toStrictEqual(TEST_SERVER_NAME) - expect(ev.connection.parsedAsyncAPI).toStrictEqual(TEST_ASYNCAPI_DOCUMENT) + const adapter = initializeAdapter() + adapter.emit('close', { + channels: ['fake/channel'], + connection: RAW_CONN, }) - adapter.emit('close', { channels: ['fake/channel'], connection: RAW_CONN }) }) }) - + describe('on("reconnect")', () => { it('notifies the Glee app', async () => { - const app = new Glee() - const adapter = new GleeAdapter(app, TEST_SERVER_NAME, TEST_SERVER, TEST_ASYNCAPI_DOCUMENT) - app.on('adapter:reconnect', (ev) => { - expect(ev.serverName).toStrictEqual(TEST_SERVER_NAME) - expect(ev.server).toStrictEqual(TEST_SERVER) - expect(ev.connection).toBeInstanceOf(GleeConnection) - expect(ev.connection.AsyncAPIServer).toStrictEqual(TEST_SERVER) - expect(ev.connection.rawConnection).toStrictEqual(RAW_CONN) - expect(ev.connection.channels).toStrictEqual(['fake/channel']) - expect(ev.connection.serverName).toStrictEqual(TEST_SERVER_NAME) - expect(ev.connection.parsedAsyncAPI).toStrictEqual(TEST_ASYNCAPI_DOCUMENT) + const adapter = initializeAdapter() + adapter.emit('reconnect', { + channels: ['fake/channel'], + connection: RAW_CONN, }) - adapter.emit('reconnect', { channels: ['fake/channel'], connection: RAW_CONN }) }) }) - + describe('getSubscribedChannels()', () => { it('returns the list of channels to which the adapter is subscribed', async () => { const app = new Glee() - const adapter = new GleeAdapter(app, TEST_SERVER_NAME, TEST_SERVER, TEST_ASYNCAPI_DOCUMENT) + const adapter = new GleeAdapter( + app, + TEST_SERVER_NAME, + TEST_SERVER, + TEST_ASYNCAPI_DOCUMENT + ) expect(adapter.getSubscribedChannels()).toStrictEqual(['test/channel']) }) }) - + describe('connect()', () => { it('throws', async () => { const app = new Glee() - const adapter = new GleeAdapter(app, TEST_SERVER_NAME, TEST_SERVER, TEST_ASYNCAPI_DOCUMENT) - await expect(adapter.connect()).rejects.toThrowError(new Error('Method `connect` is not implemented.')) + const adapter = new GleeAdapter( + app, + TEST_SERVER_NAME, + TEST_SERVER, + TEST_ASYNCAPI_DOCUMENT + ) + await expect(adapter.connect()).rejects.toThrowError( + new Error('Method `connect` is not implemented.') + ) }) }) - + describe('send()', () => { it('throws', async () => { const msg = new GleeMessage({ - payload: 'test' + payload: 'test', }) const app = new Glee() - const adapter = new GleeAdapter(app, TEST_SERVER_NAME, TEST_SERVER, TEST_ASYNCAPI_DOCUMENT) - await expect(adapter.send(msg)).rejects.toThrowError(new Error('Method `send` is not implemented.')) + const adapter = new GleeAdapter( + app, + TEST_SERVER_NAME, + TEST_SERVER, + TEST_ASYNCAPI_DOCUMENT + ) + await expect(adapter.send(msg)).rejects.toThrowError( + new Error('Method `send` is not implemented.') + ) }) }) -}) \ No newline at end of file +})