From b46379e5946b8ce10e907588399350b8ba256c4c Mon Sep 17 00:00:00 2001 From: Denis Badurina Date: Thu, 9 Jun 2022 17:06:08 +0200 Subject: [PATCH] feat: Descriptive invalid message errors Closes #366 --- docs/modules/client.md | 7 + docs/modules/common.md | 25 +- src/__tests__/__snapshots__/client.ts.snap | 5 + src/__tests__/__snapshots__/common.ts.snap | 95 +++++++ src/__tests__/client.ts | 4 +- src/__tests__/common.ts | 293 +++++++++++++++++++++ src/common.ts | 278 ++++++++++++++----- src/utils.ts | 58 ++-- 8 files changed, 657 insertions(+), 108 deletions(-) create mode 100644 src/__tests__/__snapshots__/client.ts.snap create mode 100644 src/__tests__/__snapshots__/common.ts.snap create mode 100644 src/__tests__/common.ts diff --git a/docs/modules/client.md b/docs/modules/client.md index b7757c56..7780cc70 100644 --- a/docs/modules/client.md +++ b/docs/modules/client.md @@ -29,6 +29,7 @@ - [isMessage](client.md#ismessage) - [parseMessage](client.md#parsemessage) - [stringifyMessage](client.md#stringifymessage) +- [validateMessage](client.md#validatemessage) ### Interfaces @@ -489,3 +490,9 @@ ___ ### stringifyMessage Re-exports [stringifyMessage](common.md#stringifymessage) + +___ + +### validateMessage + +Re-exports [validateMessage](common.md#validatemessage) diff --git a/docs/modules/common.md b/docs/modules/common.md index dbfdb4f0..a598d967 100644 --- a/docs/modules/common.md +++ b/docs/modules/common.md @@ -41,6 +41,7 @@ - [isMessage](common.md#ismessage) - [parseMessage](common.md#parsemessage) - [stringifyMessage](common.md#stringifymessage) +- [validateMessage](common.md#validatemessage) ## Common @@ -134,7 +135,9 @@ ___ ▸ **isMessage**(`val`): val is ConnectionInitMessage \| ConnectionAckMessage \| PingMessage \| PongMessage \| SubscribeMessage \| NextMessage \| ErrorMessage \| CompleteMessage -Checks if the provided value is a message. +Checks if the provided value is a valid GraphQL over WebSocket message. + +**`deprecated`** Use `validateMessage` instead. #### Parameters @@ -189,3 +192,23 @@ Stringifies a valid message ready to be sent through the socket. #### Returns `string` + +___ + +### validateMessage + +▸ **validateMessage**(`val`): [`Message`](common.md#message) + +Validates the message against the GraphQL over WebSocket Protocol. + +Invalid messages will throw descriptive errors. + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `val` | `unknown` | + +#### Returns + +[`Message`](common.md#message) diff --git a/src/__tests__/__snapshots__/client.ts.snap b/src/__tests__/__snapshots__/client.ts.snap new file mode 100644 index 00000000..32bd946a --- /dev/null +++ b/src/__tests__/__snapshots__/client.ts.snap @@ -0,0 +1,5 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should report close error even if complete message followed 1`] = `"\\"error\\" message expects the 'payload' property to be an array of GraphQL errors, but got \\"malformed\\""`; + +exports[`should report close error even if complete message followed 2`] = `"\\"error\\" message expects the 'payload' property to be an array of GraphQL errors, but got \\"malformed\\""`; diff --git a/src/__tests__/__snapshots__/common.ts.snap b/src/__tests__/__snapshots__/common.ts.snap new file mode 100644 index 00000000..c19383fd --- /dev/null +++ b/src/__tests__/__snapshots__/common.ts.snap @@ -0,0 +1,95 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should report invalid messages with descriptive errors 1`] = `"Message is missing the 'type' property"`; + +exports[`should report invalid messages with descriptive errors 2`] = `"Message is expected to be an object, but got string"`; + +exports[`should report invalid messages with descriptive errors 3`] = `"Message is expected to be an object, but got array"`; + +exports[`should report invalid messages with descriptive errors 4`] = `"Message is expected to be an object, but got number"`; + +exports[`should report invalid messages with descriptive errors 5`] = `"Message is expected to be an object, but got number"`; + +exports[`should report invalid messages with descriptive errors 6`] = `"Message is expected to be an object, but got function"`; + +exports[`should report invalid messages with descriptive errors 7`] = `"Message is expected to be an object, but got function"`; + +exports[`should report invalid messages with descriptive errors 8`] = `"Message is expected to be an object, but got function"`; + +exports[`should report invalid messages with descriptive errors 9`] = `"Message is missing the 'type' property"`; + +exports[`should report invalid messages with descriptive errors 10`] = `"Message is missing the 'type' property"`; + +exports[`should report invalid messages with descriptive errors 11`] = `"Message is missing the 'type' property"`; + +exports[`should report invalid messages with descriptive errors 12`] = `"Invalid message 'type' property \\"nuxt\\""`; + +exports[`should report invalid messages with descriptive errors 13`] = `"\\"connection_init\\" message expects the 'payload' property to be an object or missing, but got \\"\\""`; + +exports[`should report invalid messages with descriptive errors 14`] = `"\\"connection_init\\" message expects the 'payload' property to be an object or missing, but got \\"0\\""`; + +exports[`should report invalid messages with descriptive errors 15`] = `"\\"connection_init\\" message expects the 'payload' property to be an object or missing, but got \\"undefined\\""`; + +exports[`should report invalid messages with descriptive errors 16`] = `"\\"connection_ack\\" message expects the 'payload' property to be an object or missing, but got \\"\\""`; + +exports[`should report invalid messages with descriptive errors 17`] = `"\\"ping\\" message expects the 'payload' property to be an object or missing, but got \\"0\\""`; + +exports[`should report invalid messages with descriptive errors 18`] = `"\\"pong\\" message expects the 'payload' property to be an object or missing, but got \\"undefined\\""`; + +exports[`should report invalid messages with descriptive errors 19`] = `"\\"subscribe\\" message expects the 'id' property to be a string, but got undefined"`; + +exports[`should report invalid messages with descriptive errors 20`] = `"\\"subscribe\\" message expects the 'id' property to be a string, but got number"`; + +exports[`should report invalid messages with descriptive errors 21`] = `"\\"subscribe\\" message requires a non-empty 'id' property"`; + +exports[`should report invalid messages with descriptive errors 22`] = `"\\"subscribe\\" message expects the 'payload' property to be an object, but got undefined"`; + +exports[`should report invalid messages with descriptive errors 23`] = `"\\"subscribe\\" message expects the 'payload' property to be an object, but got array"`; + +exports[`should report invalid messages with descriptive errors 24`] = `"\\"subscribe\\" message expects the 'payload' property to be an object, but got string"`; + +exports[`should report invalid messages with descriptive errors 25`] = `"\\"subscribe\\" message payload expects the 'query' property to be a string, but got undefined"`; + +exports[`should report invalid messages with descriptive errors 26`] = `"\\"subscribe\\" message payload expects the 'query' property to be a string, but got number"`; + +exports[`should report invalid messages with descriptive errors 27`] = `"\\"subscribe\\" message payload expects the 'query' property to be a string, but got object"`; + +exports[`should report invalid messages with descriptive errors 28`] = `"\\"subscribe\\" message payload expects the 'operationName' property to be a string or nullish or missing, but got number"`; + +exports[`should report invalid messages with descriptive errors 29`] = `"\\"subscribe\\" message payload expects the 'operationName' property to be a string or nullish or missing, but got object"`; + +exports[`should report invalid messages with descriptive errors 30`] = `"\\"subscribe\\" message payload expects the 'variables' property to be a an object or nullish or missing, but got string"`; + +exports[`should report invalid messages with descriptive errors 31`] = `"\\"subscribe\\" message payload expects the 'extensions' property to be a an object or nullish or missing, but got string"`; + +exports[`should report invalid messages with descriptive errors 32`] = `"\\"subscribe\\" message payload expects the 'extensions' property to be a an object or nullish or missing, but got number"`; + +exports[`should report invalid messages with descriptive errors 33`] = `"\\"next\\" message expects the 'id' property to be a string, but got undefined"`; + +exports[`should report invalid messages with descriptive errors 34`] = `"\\"next\\" message expects the 'id' property to be a string, but got undefined"`; + +exports[`should report invalid messages with descriptive errors 35`] = `"\\"next\\" message requires a non-empty 'id' property"`; + +exports[`should report invalid messages with descriptive errors 36`] = `"\\"next\\" message expects the 'payload' property to be an object, but got undefined"`; + +exports[`should report invalid messages with descriptive errors 37`] = `"\\"next\\" message expects the 'payload' property to be an object, but got string"`; + +exports[`should report invalid messages with descriptive errors 38`] = `"\\"error\\" message expects the 'id' property to be a string, but got undefined"`; + +exports[`should report invalid messages with descriptive errors 39`] = `"\\"error\\" message requires a non-empty 'id' property"`; + +exports[`should report invalid messages with descriptive errors 40`] = `"\\"error\\" message expects the 'payload' property to be an array of GraphQL errors, but got undefined"`; + +exports[`should report invalid messages with descriptive errors 41`] = `"\\"error\\" message expects the 'payload' property to be an array of GraphQL errors, but got {}"`; + +exports[`should report invalid messages with descriptive errors 42`] = `"\\"error\\" message expects the 'payload' property to be an array of GraphQL errors, but got \\"\\""`; + +exports[`should report invalid messages with descriptive errors 43`] = `"\\"error\\" message expects the 'payload' property to be an array of GraphQL errors, but got []"`; + +exports[`should report invalid messages with descriptive errors 44`] = `"\\"error\\" message expects the 'payload' property to be an array of GraphQL errors, but got [{\\"iam\\":\\"invalid\\"}]"`; + +exports[`should report invalid messages with descriptive errors 45`] = `"\\"complete\\" message expects the 'id' property to be a string, but got undefined"`; + +exports[`should report invalid messages with descriptive errors 46`] = `"\\"complete\\" message requires a non-empty 'id' property"`; + +exports[`should report invalid messages with descriptive errors 47`] = `"\\"complete\\" message expects the 'id' property to be a string, but got number"`; diff --git a/src/__tests__/client.ts b/src/__tests__/client.ts index d97051c8..fbdb6a51 100644 --- a/src/__tests__/client.ts +++ b/src/__tests__/client.ts @@ -604,7 +604,7 @@ it('should report close error even if complete message followed', async (done) = on: { closed: (err) => { expect((err as CloseEvent).code).toBe(CloseCode.BadResponse); - expect((err as CloseEvent).reason).toBe('Invalid message'); + expect((err as CloseEvent).reason).toMatchSnapshot(); done(); }, @@ -618,7 +618,7 @@ it('should report close error even if complete message followed', async (done) = { next: noop, error: (err) => { - expect((err as Error).message).toBe('Invalid message'); + expect((err as Error).message).toMatchSnapshot(); client.dispose(); }, complete: noop, diff --git a/src/__tests__/common.ts b/src/__tests__/common.ts new file mode 100644 index 00000000..4d45e299 --- /dev/null +++ b/src/__tests__/common.ts @@ -0,0 +1,293 @@ +import { validateMessage, MessageType } from '../common'; + +it('should report invalid messages with descriptive errors', () => { + for (const invalidMessage of [ + // straight up invalid + {}, + '', + [], + 0, + 9, + Symbol, + Object, + () => { + /**/ + }, + + // invalid 'type' prop + { + type: '', + }, + { + type: undefined, + }, + { + type: 0, + }, + { + type: 'nuxt', + }, + + // invalid connection_init, connection_ack, ping and pong message + { + type: MessageType.ConnectionInit, + payload: '', + }, + { + type: MessageType.ConnectionInit, + payload: 0, + }, + { + type: MessageType.ConnectionInit, + payload: undefined, + }, + { + type: MessageType.ConnectionAck, + payload: '', + }, + { + type: MessageType.Ping, + payload: 0, + }, + { + type: MessageType.Pong, + payload: undefined, + }, + + // invalid subscribe message + { + type: MessageType.Subscribe, + }, + { + id: 0, + type: MessageType.Subscribe, + }, + { + id: '', + type: MessageType.Subscribe, + }, + { + id: 'id', + type: MessageType.Subscribe, + }, + { + id: 'id', + type: MessageType.Subscribe, + payload: [], + }, + { + id: 'id', + type: MessageType.Subscribe, + payload: '', + }, + { + id: 'id', + type: MessageType.Subscribe, + payload: {}, + }, + { + id: 'id', + type: MessageType.Subscribe, + payload: { + query: 0, + }, + }, + { + id: 'id', + type: MessageType.Subscribe, + payload: { + query: {}, + }, + }, + { + id: 'id', + type: MessageType.Subscribe, + payload: { + operationName: 0, + query: '', + }, + }, + { + id: 'id', + type: MessageType.Subscribe, + payload: { + operationName: {}, + query: '', + }, + }, + { + id: 'id', + type: MessageType.Subscribe, + payload: { + query: '', + variables: '', + }, + }, + { + id: 'id', + type: MessageType.Subscribe, + payload: { + query: '', + extensions: '', + }, + }, + { + id: 'id', + type: MessageType.Subscribe, + payload: { + query: '', + extensions: 0, + }, + }, + + // invalid next message + { + type: MessageType.Next, + }, + { + id: undefined, + type: MessageType.Next, + }, + { + id: '', + type: MessageType.Next, + }, + { + id: 'id', + type: MessageType.Next, + }, + { + id: 'id', + type: MessageType.Next, + payload: '', + }, + + // invalid error message + { + type: MessageType.Error, + }, + { + id: '', + type: MessageType.Error, + }, + { + id: 'id', + type: MessageType.Error, + }, + { + id: 'id', + type: MessageType.Error, + payload: {}, + }, + { + id: 'id', + type: MessageType.Error, + payload: '', + }, + { + id: 'id', + type: MessageType.Error, + payload: [], + }, + { + id: 'id', + type: MessageType.Error, + payload: [{ iam: 'invalid' }], + }, + + // invalid complete message + { + type: MessageType.Complete, + }, + { + id: '', + type: MessageType.Complete, + }, + { + id: 0, + type: MessageType.Complete, + }, + ]) { + expect(() => + validateMessage(invalidMessage), + ).toThrowErrorMatchingSnapshot(); + } +}); + +it('should accept valid messages', () => { + for (const valid of [ + // valid connection_init, connection_ack, ping and pong message + { + type: MessageType.ConnectionInit, + }, + { + type: MessageType.ConnectionInit, + payload: {}, + }, + { + type: MessageType.ConnectionAck, + }, + { + type: MessageType.ConnectionAck, + payload: {}, + }, + { + type: MessageType.Ping, + }, + { + type: MessageType.Ping, + payload: {}, + }, + { + type: MessageType.Pong, + }, + { + type: MessageType.Pong, + payload: {}, + }, + + // valid subscribe message + { + id: 'id', + type: MessageType.Subscribe, + payload: { + query: '', + }, + }, + { + id: 'id', + type: MessageType.Subscribe, + payload: { + operationName: undefined, + variables: undefined, + extensions: undefined, + query: '', + }, + }, + { + id: 'id', + type: MessageType.Subscribe, + payload: { + operationName: null, + variables: null, + extensions: null, + query: '', + }, + }, + + // valid error message + { + id: 'id', + type: MessageType.Error, + payload: [{ message: 'I am Error' }], + }, + + // valid complete message + { + id: 'id', + type: MessageType.Complete, + }, + ]) { + expect(() => validateMessage(valid)).not.toThrow(); + } +}); diff --git a/src/common.ts b/src/common.ts index 9569ac1d..f2e8c1df 100644 --- a/src/common.ts +++ b/src/common.ts @@ -5,13 +5,7 @@ */ import { GraphQLError } from 'graphql'; -import { - isObject, - areGraphQLErrors, - hasOwnProperty, - hasOwnObjectProperty, - hasOwnStringProperty, -} from './utils'; +import { areGraphQLErrors, extendedTypeof, isObject } from './utils'; /** * The WebSocket sub-protocol used for the [GraphQL over WebSocket Protocol](/PROTOCOL.md). @@ -205,66 +199,214 @@ export type Message = : never; /** - * Checks if the provided value is a message. + * Validates the message against the GraphQL over WebSocket Protocol. + * + * Invalid messages will throw descriptive errors. * * @category Common */ -export function isMessage(val: unknown): val is Message { - if (isObject(val)) { - // all messages must have the `type` prop - if (!hasOwnStringProperty(val, 'type')) { - return false; +export function validateMessage(val: unknown): Message { + if (!isObject(val)) { + throw new Error( + `Message is expected to be an object, but got ${extendedTypeof(val)}`, + ); + } + + if (!val.type) { + throw new Error(`Message is missing the 'type' property`); + } + if (typeof val.type !== 'string') { + throw new Error( + `Message is expects the 'type' property to be a string, but got ${extendedTypeof( + val.type, + )}`, + ); + } + + switch (val.type) { + case MessageType.ConnectionInit: + case MessageType.ConnectionAck: + case MessageType.Ping: + case MessageType.Pong: { + if ('payload' in val && !isObject(val.payload)) { + throw new Error( + `"${val.type}" message expects the 'payload' property to be an object or missing, but got "${val.payload}"`, + ); + } + + break; } - // validate other properties depending on the `type` - switch (val.type) { - case MessageType.ConnectionInit: - // the connection init message can have optional payload object - return ( - !hasOwnProperty(val, 'payload') || - val.payload === undefined || - isObject(val.payload) + + case MessageType.Subscribe: { + if (typeof val.id !== 'string') { + throw new Error( + `"${ + val.type + }" message expects the 'id' property to be a string, but got ${extendedTypeof( + val.id, + )}`, ); - case MessageType.ConnectionAck: - case MessageType.Ping: - case MessageType.Pong: - // the connection ack, ping and pong messages can have optional payload object too - return ( - !hasOwnProperty(val, 'payload') || - val.payload === undefined || - isObject(val.payload) + } + if (!val.id) { + throw new Error( + `"${val.type}" message requires a non-empty 'id' property`, ); - case MessageType.Subscribe: - return ( - hasOwnStringProperty(val, 'id') && - hasOwnObjectProperty(val, 'payload') && - (!hasOwnProperty(val.payload, 'operationName') || - val.payload.operationName === undefined || - val.payload.operationName === null || - typeof val.payload.operationName === 'string') && - hasOwnStringProperty(val.payload, 'query') && - (!hasOwnProperty(val.payload, 'variables') || - val.payload.variables === undefined || - val.payload.variables === null || - hasOwnObjectProperty(val.payload, 'variables')) && - (!hasOwnProperty(val.payload, 'extensions') || - val.payload.extensions === undefined || - val.payload.extensions === null || - hasOwnObjectProperty(val.payload, 'extensions')) + } + + if (!isObject(val.payload)) { + throw new Error( + `"${ + val.type + }" message expects the 'payload' property to be an object, but got ${extendedTypeof( + val.payload, + )}`, ); - case MessageType.Next: - return ( - hasOwnStringProperty(val, 'id') && - hasOwnObjectProperty(val, 'payload') + } + + if (typeof val.payload.query !== 'string') { + throw new Error( + `"${ + val.type + }" message payload expects the 'query' property to be a string, but got ${extendedTypeof( + val.payload.query, + )}`, + ); + } + + if (val.payload.variables != null && !isObject(val.payload.variables)) { + throw new Error( + `"${ + val.type + }" message payload expects the 'variables' property to be a an object or nullish or missing, but got ${extendedTypeof( + val.payload.variables, + )}`, ); - case MessageType.Error: - return hasOwnStringProperty(val, 'id') && areGraphQLErrors(val.payload); - case MessageType.Complete: - return hasOwnStringProperty(val, 'id'); - default: - return false; + } + + if ( + val.payload.operationName != null && + extendedTypeof(val.payload.operationName) !== 'string' + ) { + throw new Error( + `"${ + val.type + }" message payload expects the 'operationName' property to be a string or nullish or missing, but got ${extendedTypeof( + val.payload.operationName, + )}`, + ); + } + + if (val.payload.extensions != null && !isObject(val.payload.extensions)) { + throw new Error( + `"${ + val.type + }" message payload expects the 'extensions' property to be a an object or nullish or missing, but got ${extendedTypeof( + val.payload.extensions, + )}`, + ); + } + + break; } + + case MessageType.Next: { + if (typeof val.id !== 'string') { + throw new Error( + `"${ + val.type + }" message expects the 'id' property to be a string, but got ${extendedTypeof( + val.id, + )}`, + ); + } + if (!val.id) { + throw new Error( + `"${val.type}" message requires a non-empty 'id' property`, + ); + } + + if (!isObject(val.payload)) { + throw new Error( + `"${ + val.type + }" message expects the 'payload' property to be an object, but got ${extendedTypeof( + val.payload, + )}`, + ); + } + + break; + } + + case MessageType.Error: { + if (typeof val.id !== 'string') { + throw new Error( + `"${ + val.type + }" message expects the 'id' property to be a string, but got ${extendedTypeof( + val.id, + )}`, + ); + } + if (!val.id) { + throw new Error( + `"${val.type}" message requires a non-empty 'id' property`, + ); + } + + if (!areGraphQLErrors(val.payload)) { + throw new Error( + `"${ + val.type + }" message expects the 'payload' property to be an array of GraphQL errors, but got ${JSON.stringify( + val.payload, + )}`, + ); + } + + break; + } + + case MessageType.Complete: { + if (typeof val.id !== 'string') { + throw new Error( + `"${ + val.type + }" message expects the 'id' property to be a string, but got ${extendedTypeof( + val.id, + )}`, + ); + } + if (!val.id) { + throw new Error( + `"${val.type}" message requires a non-empty 'id' property`, + ); + } + + break; + } + + default: + throw new Error(`Invalid message 'type' property "${val.type}"`); + } + + return val as unknown as Message; +} + +/** + * Checks if the provided value is a valid GraphQL over WebSocket message. + * + * @deprecated Use `validateMessage` instead. + * + * @category Common + */ +export function isMessage(val: unknown): val is Message { + try { + validateMessage(val); + return true; + } catch { + return false; } - return false; } /** @@ -287,17 +429,15 @@ export function parseMessage( data: unknown, reviver?: JSONMessageReviver, ): Message { - if (isMessage(data)) { - return data; - } - if (typeof data !== 'string') { - throw new Error('Message not parsable'); - } - const message = JSON.parse(data, reviver); - if (!isMessage(message)) { - throw new Error('Invalid message'); + try { + return validateMessage(data); + } catch { + if (typeof data !== 'string') { + throw new Error('Only strings are parsable messages'); + } + const message = JSON.parse(data, reviver); + return validateMessage(message); } - return message; } /** @@ -320,8 +460,6 @@ export function stringifyMessage( msg: Message, replacer?: JSONMessageReplacer, ): string { - if (!isMessage(msg)) { - throw new Error('Cannot stringify invalid message'); - } + validateMessage(msg); return JSON.stringify(msg, replacer); } diff --git a/src/utils.ts b/src/utils.ts index 38dd925d..1762a4d4 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -5,12 +5,32 @@ */ import { GraphQLError } from 'graphql'; -// Extremely small optimisation, reduces runtime prototype traversal -const baseHasOwnProperty = Object.prototype.hasOwnProperty; +/** @private */ +export function extendedTypeof( + val: unknown, +): + | 'string' + | 'number' + | 'bigint' + | 'boolean' + | 'symbol' + | 'undefined' + | 'object' + | 'function' + | 'array' + | 'null' { + if (val === null) { + return 'null'; + } + if (Array.isArray(val)) { + return 'array'; + } + return typeof val; +} /** @private */ export function isObject(val: unknown): val is Record { - return typeof val === 'object' && val !== null; + return extendedTypeof(val) === 'object'; } /** @private */ @@ -45,38 +65,6 @@ export function areGraphQLErrors(obj: unknown): obj is readonly GraphQLError[] { ); } -/** @private */ -export function hasOwnProperty< - O extends Record, - P extends PropertyKey, ->(obj: O, prop: P): obj is O & Record { - return baseHasOwnProperty.call(obj, prop); -} - -/** @private */ -export function hasOwnObjectProperty< - O extends Record, - P extends PropertyKey, ->(obj: O, prop: P): obj is O & Record> { - return baseHasOwnProperty.call(obj, prop) && isObject(obj[prop]); -} - -/** @private */ -export function hasOwnArrayProperty< - O extends Record, - P extends PropertyKey, ->(obj: O, prop: P): obj is O & Record { - return baseHasOwnProperty.call(obj, prop) && Array.isArray(obj[prop]); -} - -/** @private */ -export function hasOwnStringProperty< - O extends Record, - P extends PropertyKey, ->(obj: O, prop: P): obj is O & Record { - return baseHasOwnProperty.call(obj, prop) && typeof obj[prop] === 'string'; -} - /** * Limits the WebSocket close event reason to not exceed a length of one frame. * Reference: https://datatracker.ietf.org/doc/html/rfc6455#section-5.2.