diff --git a/package.json b/package.json index de9e1c149..8a76914b7 100644 --- a/package.json +++ b/package.json @@ -28,13 +28,6 @@ "default": "./dist/esm/index.mjs" } }, - "browser": { - "crypto": false, - "https": false, - "jsonwebtoken": false, - "ws": false, - "zlib": false - }, "license": "SEE LICENSE IN LICENSE", "keywords": [ "chat", @@ -51,15 +44,8 @@ ], "dependencies": { "@stream-io/logger": "^2.0.0", - "@types/jsonwebtoken": "^9.0.8", - "@types/ws": "^8.18.1", "axios": "^1.16.1", - "base64-js": "^1.5.1", - "form-data": "^4.0.5", - "isomorphic-ws": "^5.0.0", - "jsonwebtoken": "^9.0.3", - "linkifyjs": "^4.3.3", - "ws": "^8.20.1" + "linkifyjs": "^4.3.3" }, "devDependencies": { "@commitlint/cli": "^21.0.1", @@ -67,7 +53,7 @@ "@eslint/js": "^9.39.4", "@semantic-release/changelog": "^6.0.3", "@semantic-release/git": "^10.0.1", - "@types/node": "^22.19.19", + "@types/node": "^22", "@types/sinon": "^10.0.6", "@vitest/coverage-v8": "^4.1.7", "concurrently": "^9.2.1", diff --git a/scripts/bundle.mjs b/scripts/bundle.mjs index 5fa496dbe..c22b71dfd 100755 --- a/scripts/bundle.mjs +++ b/scripts/bundle.mjs @@ -21,7 +21,7 @@ const modules = Object.keys({ // do not externalize modules that are ignored in browser field // externalizing them will cause esbuild to not replace the imports // in the bundles -const browserIgnoreModules = Object.keys(packageJson.browser); +const browserIgnoreModules = []; // Object.keys(packageJson.browser); const browserExternal = modules.filter( (module) => !browserIgnoreModules.includes(module), ); diff --git a/src/api-client.ts b/src/api-client.ts index 3cb41fdc9..ee24d824a 100644 --- a/src/api-client.ts +++ b/src/api-client.ts @@ -82,7 +82,7 @@ export class ApiClient { sendFile( url: string, - uri: string | NodeJS.ReadableStream | Buffer | File, + uri: string | File, name?: string, contentType?: string, user?: UserResponse, @@ -92,7 +92,6 @@ export class ApiClient { if (user != null) data.append('user', JSON.stringify(user)); return this._doRequest('post', url, data, { - headers: data.getHeaders ? data.getHeaders() : {}, timeout: 0, maxContentLength: Infinity, maxBodyLength: Infinity, diff --git a/src/base64.ts b/src/base64.ts deleted file mode 100644 index 472ba73ae..000000000 --- a/src/base64.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { fromByteArray } from 'base64-js'; - -function isString(arrayOrString: string | T[]): arrayOrString is string { - return typeof (arrayOrString as string) === 'string'; -} - -type MapGenericCallback = (value: T, index: number, array: T[]) => U; -type MapStringCallback = (value: string, index: number, string: string) => U; - -function isMapStringCallback( - arrayOrString: string | T[], - callback: MapGenericCallback | MapStringCallback, -): callback is MapStringCallback { - return !!callback && isString(arrayOrString); -} - -// source - https://github.com/beatgammit/base64-js/blob/master/test/convert.js#L72 -function map(array: T[], callback: MapGenericCallback): U[]; -function map(string: string, callback: MapStringCallback): U[]; -function map( - arrayOrString: string | T[], - callback: MapGenericCallback | MapStringCallback, -): U[] { - const res = []; - - if (isString(arrayOrString) && isMapStringCallback(arrayOrString, callback)) { - for (let k = 0, len = arrayOrString.length; k < len; k++) { - if (arrayOrString.charAt(k)) { - const kValue = arrayOrString.charAt(k); - const mappedValue = callback(kValue, k, arrayOrString); - res[k] = mappedValue; - } - } - } else if (!isString(arrayOrString) && !isMapStringCallback(arrayOrString, callback)) { - for (let k = 0, len = arrayOrString.length; k < len; k++) { - if (k in arrayOrString) { - const kValue = arrayOrString[k]; - const mappedValue = callback(kValue, k, arrayOrString); - res[k] = mappedValue; - } - } - } - - return res; -} - -export const encodeBase64 = (data: string): string => - fromByteArray(new Uint8Array(map(data, (char) => char.charCodeAt(0)))); - -// base-64 decoder throws exception if encoded string is not padded by '=' to make string length -// in multiples of 4. So gonna use our own method for this purpose to keep backwards compatibility -// https://github.com/beatgammit/base64-js/blob/master/index.js#L26 -export const decodeBase64 = (s: string): string => { - const e = {} as { [key: string]: number }, - w = String.fromCharCode, - L = s.length; - let i, - b = 0, - c, - x, - l = 0, - a, - r = ''; - const A = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; - for (i = 0; i < 64; i++) { - e[A.charAt(i)] = i; - } - for (x = 0; x < L; x++) { - c = e[s.charAt(x)]; - b = (b << 6) + c; - l += 6; - while (l >= 8) { - // eslint-disable-next-line @typescript-eslint/no-unused-expressions - ((a = (b >>> (l -= 8)) & 0xff) || x < L - 2) && (r += w(a)); - } - } - return r; -}; diff --git a/src/channel.ts b/src/channel.ts index 0a023e8ec..889d61e0a 100644 --- a/src/channel.ts +++ b/src/channel.ts @@ -497,13 +497,13 @@ export class Channel extends ChannelApi { * * @param uri - File source: URL string, `File`, `Buffer`, or readable stream (Node). * @param name - File name sent in the multipart body (optional). - * @param contentType - MIME type; defaults are applied when omitted (optional). + * @param contentType - MIME type; required for React Native URI uploads (optional). * @param user - User payload appended to the form as JSON (optional). * @param axiosRequestConfig - Axios per-request config, merged after upload defaults, e.g. `onUploadProgress`, `signal` from `AbortController` (optional). * @returns A promise resolving to `{ file: string, ... }` with the CDN URL. */ sendFile( - uri: string | NodeJS.ReadableStream | Buffer | File, + uri: string | File, name?: string, contentType?: string, user?: UserResponse, @@ -524,13 +524,13 @@ export class Channel extends ChannelApi { * * @param uri - Image source: URL string, `File`, or readable stream (Node). For `Buffer` uploads, use `sendFile` toward the channel file endpoint instead. * @param name - File name sent in the multipart body (optional). - * @param contentType - MIME type (optional). + * @param contentType - MIME type; required for React Native URI uploads (optional). * @param user - User payload appended to the form as JSON (optional). * @param axiosRequestConfig - Axios per-request config, merged after upload defaults, e.g. `onUploadProgress`, `signal` (optional). * @returns A promise resolving to `{ file: string, ... }` with the CDN URL. */ sendImage( - uri: string | NodeJS.ReadableStream | File, + uri: string | File, name?: string, contentType?: string, user?: UserResponse, diff --git a/src/client.ts b/src/client.ts index be680e1fd..1ad7e207b 100644 --- a/src/client.ts +++ b/src/client.ts @@ -3,7 +3,6 @@ import type { AxiosInstance, AxiosRequestConfig } from 'axios'; import axios from 'axios'; -import https from 'https'; import { Channel } from './channel'; import { ClientState } from './client_state'; @@ -159,7 +158,7 @@ export class StreamChat extends ChatApi { axiosInstance: AxiosInstance; baseURL?: string; browser: boolean; - cleaningIntervalRef?: NodeJS.Timeout; + cleaningIntervalRef?: ReturnType; clientId?: string; key: string; listeners: Map>; @@ -248,7 +247,7 @@ export class StreamChat extends ChatApi { * @param options.logLevel - Minimum log level for the default sink (optional, defaults to `'info'`). * @param options.logOptions - Per-scope sink/level overrides for `chatLoggerSystem` (optional). * @param options.timeout - Request timeout (optional, defaults to `3000`). - * @param options.httpsAgent - Custom `httpsAgent` (optional, in Node defaults to `https.agent()`). + * @param options.httpsAgent - Custom `httpsAgent` (optional). */ constructor(key: string, options: StreamChatOptions = {}) { // generated client requires ApiClient right away @@ -290,9 +289,6 @@ export class StreamChat extends ChatApi { this.axiosInstance = axios.create({ timeout: 3000, withCredentials: false, - httpsAgent: this.node - ? new https.Agent({ keepAlive: true, keepAliveMsecs: 3000 }) - : undefined, ...this.options.axiosRequestConfig, paramsSerializer: axiosParamsSerializer, }); @@ -2283,13 +2279,13 @@ export class StreamChat extends ChatApi { * * @param uri - The file to upload. * @param name - The name of the file (optional). - * @param contentType - The content type of the file (optional). + * @param contentType - MIME type; required for React Native URI uploads (optional). * @param user - User information (optional). * @param axiosRequestConfig - Axios config, e.g. `onUploadProgress` for progress tracking (optional). * @returns Response containing the file URL. */ uploadFile_( - uri: string | NodeJS.ReadableStream | Buffer | File, + uri: string | File, name?: string, contentType?: string, user?: UserResponse, @@ -2310,13 +2306,13 @@ export class StreamChat extends ChatApi { * * @param uri - The image to upload. * @param name - The name of the image (optional). - * @param contentType - The content type of the image (optional). + * @param contentType - MIME type; required for React Native URI uploads (optional). * @param user - User information (optional). * @param axiosRequestConfig - Axios config, e.g. `onUploadProgress` for progress tracking (optional). * @returns Response containing the image URL. */ uploadImage_( - uri: string | NodeJS.ReadableStream | File, + uri: string | File, name?: string, contentType?: string, user?: UserResponse, diff --git a/src/connection.ts b/src/connection.ts index 6ad1de09c..69f0d2be8 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -1,4 +1,3 @@ -import WebSocket from 'isomorphic-ws'; import { addConnectionEventListeners, chatCodes, @@ -24,12 +23,22 @@ const logger = chatLoggerSystem.getLogger('connection'); // Type guards to check WebSocket error type const isCloseEvent = ( - res: WebSocket.CloseEvent | WebSocket.Data | WebSocket.ErrorEvent, -): res is WebSocket.CloseEvent => (res as WebSocket.CloseEvent).code !== undefined; + res: CloseEvent | MessageEvent | ErrorEvent | Event, +): res is CloseEvent => (res as CloseEvent).code !== undefined; const isErrorEvent = ( - res: WebSocket.CloseEvent | WebSocket.Data | WebSocket.ErrorEvent, -): res is WebSocket.ErrorEvent => (res as WebSocket.ErrorEvent).error !== undefined; + res: CloseEvent | MessageEvent | ErrorEvent | Event, +): res is ErrorEvent => (res as ErrorEvent).error !== undefined; + +class WSCloseError extends Error { + public reason?: string; + public wasClean?: boolean; + public code?: number; + public target?: EventTarget | null; + constructor(message?: string, errorOptions?: ErrorOptions) { + super(message, errorOptions); + } +} /** * A WS connection that reconnects upon failure. @@ -252,11 +261,6 @@ export class StableWSConnection { this.isHealthy = false; - // remove ws handlers... - if (this.ws && this.ws.removeAllListeners) { - this.ws.removeAllListeners(); - } - let isClosedPromise: Promise; // and finally close... // Assigning to local here because we will remove it from this before the @@ -264,7 +268,7 @@ export class StableWSConnection { const { ws } = this; if (ws && ws.close && ws.readyState === ws.OPEN) { isClosedPromise = new Promise((resolve) => { - const onclose = (event: WebSocket.CloseEvent) => { + const onclose = (event: CloseEvent) => { logger .withExtraTags('disconnect') .debug( @@ -337,7 +341,10 @@ export class StableWSConnection { wsURL, requestID: this.requestID, }); - this.ws = new WebSocket(wsURL); + + const WS = this.client.options.WebSocketImpl ?? WebSocket; + this.ws = new WS(wsURL); + this.ws.onopen = this.onopen.bind(this, this.wsID); this.ws.onclose = this.onclose.bind(this, this.wsID); this.ws.onerror = this.onerror.bind(this, this.wsID); @@ -503,7 +510,7 @@ export class StableWSConnection { }); }; - onmessage = (wsId: number, event: WebSocket.MessageEvent) => { + onmessage = (wsId: number, event: MessageEvent) => { if (this.wsID !== wsId) return; logger.withExtraTags('onmessage').trace('WebSocket onmessage callback fired.', { @@ -539,7 +546,7 @@ export class StableWSConnection { this.scheduleConnectionCheck(); }; - onclose = (wsId: number, event: WebSocket.CloseEvent) => { + onclose = (wsId: number, event: CloseEvent) => { if (this.wsID !== wsId) return; logger @@ -552,9 +559,7 @@ export class StableWSConnection { if (event.code === chatCodes.WS_CLOSED_SUCCESS) { // this is a permanent error raised by stream.. // usually caused by invalid auth details - const error = new Error( - `WS connection reject with error ${event.reason}`, - ) as Error & WebSocket.CloseEvent; + const error = new WSCloseError(`WS connection reject with error ${event.reason}`); error.reason = event.reason; error.code = event.code; @@ -584,7 +589,7 @@ export class StableWSConnection { } }; - onerror = (wsId: number, event: WebSocket.ErrorEvent) => { + onerror = (wsId: number, event: Event) => { if (this.wsID !== wsId) return; this.consecutiveFailures += 1; @@ -630,7 +635,7 @@ export class StableWSConnection { * @returns A normalized error describing the WS failure. */ _errorFromWSEvent = ( - event: WebSocket.CloseEvent | WebSocket.Data | WebSocket.ErrorEvent, + event: CloseEvent | MessageEvent | ErrorEvent | Event, isWSFailure = true, ) => { let code; @@ -679,7 +684,6 @@ export class StableWSConnection { this.wsID += 1; try { - this?.ws?.removeAllListeners(); this?.ws?.close(); } catch (e) { // we don't care diff --git a/src/index.ts b/src/index.ts index 38ae430cb..c4fc23619 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,3 @@ -export * from './base64'; export * from './client'; export * from './client_state'; export * from './channel'; diff --git a/src/signing.ts b/src/signing.ts index 35fc296d5..71abe411f 100644 --- a/src/signing.ts +++ b/src/signing.ts @@ -1,71 +1,3 @@ -import jwt from 'jsonwebtoken'; -import crypto from 'crypto'; -import zlib from 'zlib'; -import { decodeBase64, encodeBase64 } from './base64'; -import type { UR } from './types'; -import type { WSEvent } from './gen/models'; - -/** - * Creates the JWT token that can be used for a user session. - * - * @param apiSecret - API secret key. - * @param userId - The `user_id` key in the JWT payload. - * @param extraData - Extra data that should be part of the JWT token (optional, defaults to `{}`). - * @param jwtOptions - Options that can be passed to `jwt.sign` (optional, defaults to `{}`). - * @returns The signed JWT token. - */ -export function JWTUserToken( - apiSecret: jwt.Secret, - userId: string, - extraData: UR = {}, - jwtOptions: jwt.SignOptions = {}, -) { - if (typeof userId !== 'string') { - throw new TypeError('userId should be a string'); - } - - const payload: { user_id: string } & UR = { - user_id: userId, - ...extraData, - }; - - // make sure we return a clear error when the JWT module is shimmed (i.e. browser build) - if (jwt == null || jwt.sign == null) { - throw Error( - `Unable to find jwt crypto, if you are getting this error is probably because you are trying to generate tokens on browser or React Native (or other environment where crypto functions are not available). Please Note: token should only be generated server-side.`, - ); - } - - const opts: jwt.SignOptions = Object.assign( - { algorithm: 'HS256', noTimestamp: true }, - jwtOptions, - ); - - if (payload.iat) { - opts.noTimestamp = false; - } - return jwt.sign(payload, apiSecret, opts); -} - -/** - * Creates the JWT token that can be used for a server-side session. - * - * @param apiSecret - API secret key. - * @param jwtOptions - Options that can be passed to `jwt.sign` (optional, defaults to `{}`). - * @returns The signed JWT token. - */ -export function JWTServerToken(apiSecret: jwt.Secret, jwtOptions: jwt.SignOptions = {}) { - const payload = { - server: true, - }; - - const opts: jwt.SignOptions = Object.assign( - { algorithm: 'HS256', noTimestamp: true }, - jwtOptions, - ); - return jwt.sign(payload, apiSecret, opts); -} - /** * Decodes a JWT token and returns the embedded `user_id`. * @@ -78,253 +10,7 @@ export function UserFromToken(token: string) { return ''; } const b64Payload = fragments[1]; - const payload = decodeBase64(b64Payload); + const payload = atob(b64Payload); const data = JSON.parse(payload); return data.user_id as string; } - -/** - * Generates a development token for the given user. - * - * Development tokens are unsigned and must only be used in environments where token validation is disabled. - * - * @param userId - The ID of the user. - * @returns The development token. - */ -export function DevToken(userId: string) { - return [ - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9', //{"alg": "HS256", "typ": "JWT"} - encodeBase64(JSON.stringify({ user_id: userId })), - 'devtoken', // hardcoded signature - ].join('.'); -} - -/** - * Constant-time HMAC-SHA256 verification of `signature` against the digest of `body` using `secret` - * as the key. The signature is always computed over the **uncompressed** JSON bytes, so callers that - * decoded a gzipped or base64-wrapped payload must pass the inflated bytes here. - * - * The legacy `client.verifyWebhook` helper wraps this function, so callers that have already - * migrated to {@link verifyAndParseWebhook}, {@link parseSqs}, or {@link parseSns} rarely need to - * invoke this directly. - * - * @param body - The uncompressed payload bytes that Stream signed. - * @param signature - The HMAC-SHA256 signature delivered alongside the payload. - * @param secret - Your app's API secret used as the HMAC key. - * @returns `true` when the signature matches the digest of `body`, otherwise `false`. - */ -export function verifySignature( - body: string | Buffer, - signature: string, - secret: string, -): boolean { - const key = Buffer.from(secret, 'utf8'); - const hash = crypto.createHmac('sha256', key).update(body).digest('hex'); - try { - return crypto.timingSafeEqual(Buffer.from(hash), Buffer.from(signature)); - } catch { - return false; - } -} - -/** - * Verifies an HMAC-SHA256 signature with the legacy parameter order. - * - * @param body - The uncompressed payload bytes that Stream signed. - * @param secret - Your app's API secret used as the HMAC key. - * @param signature - The HMAC-SHA256 signature delivered alongside the payload. - * @returns `true` when the signature matches the digest of `body`, otherwise `false`. - * @deprecated Use {@link verifySignature} instead — same logic, parameters reordered to match the - * cross-SDK contract (`verifySignature(body, signature, secret)`). - */ -export function CheckSignature(body: string | Buffer, secret: string, signature: string) { - return verifySignature(body, signature, secret); -} - -/** - * Canonical failure-mode messages for {@link InvalidWebhookError}. - * - * Customers that prefer exact-match filtering (security logging, retry - * policy) over substring matches can compare `err.message` to these - * constants instead of pattern-matching free-form text. - */ -export const InvalidWebhookErrorMessages = { - signatureMismatch: 'signature mismatch', - invalidBase64: 'invalid base64 encoding', - gzipFailed: 'gzip decompression failed', - invalidJson: 'invalid JSON payload', -} as const; - -/** - * Thrown by {@link verifyAndParseWebhook} when the supplied `x-signature` does not - * match the HMAC of the uncompressed payload, and by all webhook helpers (including - * {@link parseSqs} / {@link parseSns}) when a gzip / base64 / JSON envelope is malformed. - * - * The message identifies which failure mode fired. See - * {@link InvalidWebhookErrorMessages} for the canonical strings. - */ -export class InvalidWebhookError extends Error { - public name = 'InvalidWebhookError'; - - constructor(message: string = InvalidWebhookErrorMessages.signatureMismatch) { - super(message); - } -} - -/** - * Returns `body` as a `Buffer`, gzip-decompressed when its first two bytes match the gzip magic - * (`1f 8b`, per RFC 1952). When the body is plain JSON (no compression, or middleware already - * decompressed), the bytes are returned unchanged. - * - * Magic-byte detection (rather than relying on a header) keeps the same handler correct when - * middleware — Express, Next.js, AWS Lambda — auto-decompresses the request before your code sees it. - * - * @param rawBody - The raw HTTP request body, either as a string or a `Buffer`. - * @returns The uncompressed payload bytes. - * @throws {@link InvalidWebhookError} when the gzip envelope is malformed. - */ -export function gunzipPayload(rawBody: string | Buffer): Buffer { - const GZIP_MAGIC = Buffer.from([0x1f, 0x8b]); - - const body = Buffer.isBuffer(rawBody) ? rawBody : Buffer.from(rawBody); - if (body.length >= 2 && body.subarray(0, 2).equals(GZIP_MAGIC)) { - try { - return zlib.gunzipSync(body); - } catch { - throw new InvalidWebhookError(InvalidWebhookErrorMessages.gzipFailed); - } - } - return body; -} - -/** - * Reverses the SQS firehose envelope: the message `Body` is base64-decoded, then the result is - * gzip-decompressed when it begins with the gzip magic. Returns the raw JSON `Buffer` Stream signed. - * - * SQS bodies are always base64-encoded so they remain valid UTF-8 over the queue. The same call - * works whether or not Stream is currently compressing payloads for this app. - * - * @param body - The base64-encoded SQS message body. - * @returns The decoded (and decompressed, when gzipped) payload bytes. - * @throws {@link InvalidWebhookError} when the body is not canonical base64 or the gzip envelope is malformed. - */ -export function decodeSqsPayload(body: string): Buffer { - // Reject anything that isn't canonical base64 up front. Node's base64 - // decoder is permissive (silently strips characters outside the - // alphabet, accepts both standard and URL-safe variants), so we have - // to be strict here to avoid silently corrupting the body before the - // signature check runs. - if (!/^[A-Za-z0-9+/]*={0,2}$/.test(body) || body.length % 4 !== 0) { - throw new InvalidWebhookError(InvalidWebhookErrorMessages.invalidBase64); - } - const decoded = Buffer.from(body, 'base64'); - if (decoded.toString('base64').length !== body.length) { - throw new InvalidWebhookError(InvalidWebhookErrorMessages.invalidBase64); - } - return gunzipPayload(decoded); -} - -/** - * Reverses an SNS HTTP notification envelope. When `notificationBody` is a JSON envelope - * (`{"Type":"Notification","MessageRequest":"..."}`), the inner `MessageRequest` field is extracted and run - * through the SQS pipeline (base64-decode, then gzip-if-magic). When the input is not a JSON - * envelope it is treated as the already-extracted `MessageRequest` string, so call sites that pre-unwrap - * continue to work. - * - * @param notificationBody - The raw SNS notification body, or a pre-extracted `MessageRequest` string. - * @returns The decoded (and decompressed, when gzipped) payload bytes. - * @throws {@link InvalidWebhookError} when the body is not canonical base64 or the gzip envelope is malformed. - */ -export function decodeSnsPayload(notificationBody: string): Buffer { - const inner = extractSnsMessage(notificationBody); - return decodeSqsPayload(inner ?? notificationBody); -} - -function extractSnsMessage(notificationBody: string): string | null { - const trimmed = notificationBody.replace(/^[\s\uFEFF]+/, ''); - if (!trimmed.startsWith('{')) { - return null; - } - let parsed: unknown; - try { - parsed = JSON.parse(trimmed); - } catch { - return null; - } - if ( - parsed === null || - typeof parsed !== 'object' || - Array.isArray(parsed) || - typeof (parsed as { MessageRequest?: unknown }).MessageRequest !== 'string' - ) { - return null; - } - return (parsed as { MessageRequest: string }).MessageRequest; -} - -/** - * Parses a JSON-encoded webhook event into a typed {@link WSEvent}. New event types Stream - * introduces still parse successfully — the runtime shape is the JSON Stream sent and the `type` - * field stays preserved. - * - * @param payload - The raw event payload bytes or string. - * @returns The parsed WebSocket event. - * @throws {@link InvalidWebhookError} when the payload is not valid JSON. - */ -export function parseEvent(payload: Buffer | string): WSEvent { - const text = Buffer.isBuffer(payload) ? payload.toString('utf8') : payload; - try { - return JSON.parse(text) as WSEvent; - } catch { - throw new InvalidWebhookError(InvalidWebhookErrorMessages.invalidJson); - } -} - -function verifyAndParse(payload: Buffer, signature: string, secret: string): WSEvent { - if (!verifySignature(payload, signature, secret)) { - throw new InvalidWebhookError(InvalidWebhookErrorMessages.signatureMismatch); - } - return parseEvent(payload); -} - -/** - * Decompress (when gzipped), verify the HMAC `signature`, and return the parsed {@link WSEvent}. - * - * @param rawBody - Raw HTTP request body bytes Stream signed. - * @param signature - Value of the `X-Signature` header. - * @param secret - Your app's API secret. - * @returns The parsed WebSocket event. - * @throws {@link InvalidWebhookError} when the signature does not match or the gzip envelope is malformed. - */ -export function verifyAndParseWebhook( - rawBody: string | Buffer, - signature: string, - secret: string, -): WSEvent { - return verifyAndParse(gunzipPayload(rawBody), signature, secret); -} - -/** - * Decodes the SQS message `Body` (base64, then gzip-if-magic) and returns the parsed {@link WSEvent}. - * Stream does not attach an application-level HMAC to SQS deliveries — use - * {@link verifyAndParseWebhook} for HTTP webhooks. - * - * @param messageBody - The base64-encoded SQS message body. - * @returns The parsed WebSocket event. - * @throws {@link InvalidWebhookError} when the body is malformed. - */ -export function parseSqs(messageBody: string): WSEvent { - return parseEvent(decodeSqsPayload(messageBody)); -} - -/** - * Decodes an SNS notification (unwraps the JSON envelope when needed; same inner format as SQS). - * No application-level HMAC verification. - * - * @param notificationBody - The raw SNS notification body, or a pre-extracted `MessageRequest` string. - * @returns The parsed WebSocket event. - * @throws {@link InvalidWebhookError} when the body is malformed. - */ -export function parseSns(notificationBody: string): WSEvent { - return parseEvent(decodeSnsPayload(notificationBody)); -} diff --git a/src/token_manager.ts b/src/token_manager.ts index 9af2fb74e..3d1e4e563 100644 --- a/src/token_manager.ts +++ b/src/token_manager.ts @@ -1,7 +1,5 @@ -import type jwt from 'jsonwebtoken'; - import { chatLoggerSystem } from './logger'; -import { JWTServerToken, JWTUserToken, UserFromToken } from './signing'; +import { UserFromToken } from './signing'; import { isFunction } from './utils'; import type { TokenOrProvider } from './types'; @@ -16,27 +14,16 @@ export type TokenManagerMinimalUser = { id: string; anon?: boolean }; export class TokenManager { loadTokenPromise: Promise | null; type: 'static' | 'provider'; - secret?: jwt.Secret; token?: string; tokenProvider?: TokenOrProvider; user?: TokenManagerMinimalUser; /** - * Initializes the token manager, optionally with a server-side API secret used to mint tokens - * locally. - * - * @param secret - Optional API secret. When provided, the manager will sign server tokens locally. + * Initializes the token manager. */ - constructor(secret?: jwt.Secret) { + constructor() { this.loadTokenPromise = null; - if (secret) { - this.secret = secret; - } this.type = 'static'; - - if (this.secret) { - this.token = JWTServerToken(this.secret); - } } /** @@ -63,11 +50,6 @@ export class TokenManager { this.type = 'static'; } - if (!tokenOrProvider && this.user && this.secret) { - this.token = JWTUserToken(this.secret, user.id, {}, {}); - this.type = 'static'; - } - await this.loadToken(); }; @@ -88,11 +70,6 @@ export class TokenManager { // allow empty token for anon user if (user && user.anon && !tokenOrProvider) return; - // Don't allow empty token for non-server side client. - if (!this.secret && !tokenOrProvider) { - throw new Error('User token can not be empty'); - } - if ( tokenOrProvider && typeof tokenOrProvider !== 'string' && @@ -158,10 +135,6 @@ export class TokenManager { return this.token; } - if (this.secret) { - return JWTServerToken(this.secret); - } - throw new Error( `Both secret and user tokens are not set. Either client.connectUser wasn't called or client.disconnect was called`, ); diff --git a/src/types.ts b/src/types.ts index 2dedec94b..cff6228f6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -417,6 +417,12 @@ export type StreamChatOptions = { * should not be used in production apps. */ wsConnection?: StableWSConnection; + /** + * Overrides the `WebSocket` constructor used by `StableWSConnection`. Intended purely for + * testing so a mock/drivable WebSocket can be swapped in; production code should leave this + * unset and rely on the platform's global `WebSocket`. + */ + WebSocketImpl?: typeof WebSocket; /** * Sets a suffix to the wsUrl when it is being built in `wsConnection`. Is meant to be * used purely in testing suites and should not be used in production apps. diff --git a/src/utils.ts b/src/utils.ts index 48ef780c4..97452f37a 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,3 @@ -import FormData from 'form-data'; import type { LocalMessage, MessageRequest, @@ -48,30 +47,6 @@ export const chatCodes = { WS_CLOSED_SUCCESS: 1000, }; -function isReadableStream(obj: unknown): obj is NodeJS.ReadStream { - return ( - obj !== null && - typeof obj === 'object' && - ((obj as NodeJS.ReadStream).readable || - typeof (obj as NodeJS.ReadStream)._read === 'function') - ); -} - -function isBuffer(obj: unknown): obj is Buffer { - return ( - obj != null && - (obj as Buffer).constructor != null && - // @ts-expect-error expected - typeof obj.constructor.isBuffer === 'function' && - // @ts-expect-error expected - obj.constructor.isBuffer(obj) - ); -} - -function isFileWebAPI(uri: unknown): uri is File { - return typeof window !== 'undefined' && 'File' in window && uri instanceof File; -} - export function isOwnUser( user?: OwnUserResponse | UserResponse, ): user is OwnUserResponse { @@ -126,22 +101,23 @@ export const userHasReadReceipts = (client: StreamChat) => client.user?.privacy_settings?.read_receipts?.enabled ?? true; export function addFileToFormData( - uri: string | NodeJS.ReadableStream | Buffer | File, + uri: string | Blob, name?: string, contentType?: string, ) { const data = new FormData(); - if (isReadableStream(uri) || isBuffer(uri) || isFileWebAPI(uri) || isBlobWebAPI(uri)) { + if (isBlobWebAPI(uri)) { if (name) data.append('file', uri, name); else data.append('file', uri); } else { + // React Native path data.append('file', { uri, - name: name || (uri as string).split('/').reverse()[0], + name: name || uri.split('/').reverse()[0], contentType: contentType || undefined, type: contentType || undefined, - }); + } as unknown as Blob); } return data; @@ -709,7 +685,7 @@ export const debounce = any>( timeout = 0, { leading = false, trailing = true }: { leading?: boolean; trailing?: boolean } = {}, ): DebouncedFunc => { - let runningTimeout: null | NodeJS.Timeout = null; + let runningTimeout: null | ReturnType = null; let argsForTrailingExecution: Parameters | null = null; let lastResult: ReturnType | undefined; @@ -835,7 +811,7 @@ export const isBlockedMessage = (message: LocalMessage) => export const isBouncedMessage = (message: LocalMessage) => message.type === 'error' && message?.moderation?.action === 'bounce'; -export const getEnv = (envKey: keyof NodeJS.ProcessEnv) => { +export const getEnv = (envKey: string) => { if ( typeof process !== 'undefined' && (Object.hasOwn(process, 'env') || 'env' in process) diff --git a/test/unit/MessageComposer/attachmentManager.test.ts b/test/unit/MessageComposer/attachmentManager.test.ts index 60ee8d9ee..ca39cd7df 100644 --- a/test/unit/MessageComposer/attachmentManager.test.ts +++ b/test/unit/MessageComposer/attachmentManager.test.ts @@ -1498,7 +1498,9 @@ describe('AttachmentManager', () => { undefined, undefined, undefined, - { signal: expect.any(AbortSignal) }, + { + signal: expect.any(AbortSignal), + }, ); }); @@ -1721,7 +1723,9 @@ describe('AttachmentManager', () => { undefined, undefined, undefined, - { onUploadProgress: expect.any(Function) }, + { + onUploadProgress: expect.any(Function), + }, ); }); @@ -1742,7 +1746,9 @@ describe('AttachmentManager', () => { undefined, undefined, undefined, - { onUploadProgress: expect.any(Function) }, + { + onUploadProgress: expect.any(Function), + }, ); }); @@ -1838,7 +1844,9 @@ describe('AttachmentManager', () => { undefined, undefined, undefined, - { signal: controller.signal }, + { + signal: controller.signal, + }, ); }); diff --git a/test/unit/client.construction.test.ts b/test/unit/client.construction.test.ts index 5ef9d55da..d531f5592 100644 --- a/test/unit/client.construction.test.ts +++ b/test/unit/client.construction.test.ts @@ -1,7 +1,6 @@ import axios from 'axios'; -import https from 'https'; import sinon from 'sinon'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { ClientState } from '../../src/client_state'; import { FixedSizeQueueCache } from '../../src/utils/FixedSizeQueueCache'; @@ -221,15 +220,13 @@ describe('StreamChat construction', () => { }); describe('httpsAgent', () => { - it('auto-creates a keep-alive https.Agent in node mode', () => { + it('does not auto-create an httpsAgent in node mode', () => { const client = new StreamChat(API_KEY, { browser: false }); - const httpsAgent = client.axiosInstance.defaults.httpsAgent as https.Agent; - expect(httpsAgent).to.be.instanceOf(https.Agent); - expect(httpsAgent.keepAlive).to.equal(true); + expect(client.axiosInstance.defaults.httpsAgent).to.be.undefined; }); - it('lets axiosRequestConfig.httpsAgent override the auto-created agent', () => { - const customAgent = new https.Agent({ keepAlive: false }); + it('honors an axiosRequestConfig.httpsAgent supplied by the caller', () => { + const customAgent = vi.fn(); const client = new StreamChat(API_KEY, { browser: false, axiosRequestConfig: { httpsAgent: customAgent }, diff --git a/test/unit/connection.test.js b/test/unit/connection.test.js index 6c5923ed2..19ba4a8f7 100644 --- a/test/unit/connection.test.js +++ b/test/unit/connection.test.js @@ -1,6 +1,5 @@ import sinon from 'sinon'; import url from 'url'; -import { Server as WsServer } from 'isomorphic-ws'; import { StableWSConnection } from '../../src/connection'; import { StreamChat } from '../../src/client'; @@ -8,14 +7,92 @@ import { TokenManager } from '../../src/token_manager'; import { sleep } from '../../src/utils'; import { InsightMetrics } from '../../src/insights'; -import { describe, expect, it, afterAll } from 'vitest'; +import { describe, expect, it } from 'vitest'; + +// A test-only WebSocket that immediately opens and pushes the canned +// `health.check` frame the real Stream backend sends on the first message. +// Used with `client.options.WebSocketImpl` so tests never touch the network. +const HEALTH_CHECK_PAYLOAD = + '{"type":"health.check","connection_id":"61112366-0a15-3891-0000-000000000009","cid":"*","me":{"id":"amin","role":"user","created_at":"2021-07-27T13:18:23.293696Z","updated_at":"2021-07-27T13:20:08.047284Z","last_active":"2021-08-11T10:42:44.213510048Z","banned":false,"online":true,"invisible":false,"devices":[],"mutes":[],"channel_mutes":[],"unread_count":98,"total_unread_count":98,"unread_channels":18,"language":"","image":"https://cdn.fakercloud.com/avatars/Shriiiiimp_128.jpg","name":"amin"},"created_at":"2021-08-11T10:42:44.222203145Z"}'; + +class MockWebSocket { + static CONNECTING = 0; + static OPEN = 1; + static CLOSING = 2; + static CLOSED = 3; + + CONNECTING = 0; + OPEN = 1; + CLOSING = 2; + CLOSED = 3; + + onopen = null; + onclose = null; + onerror = null; + onmessage = null; + + constructor(url) { + this.url = url; + this.readyState = MockWebSocket.CONNECTING; + + queueMicrotask(() => { + this.readyState = MockWebSocket.OPEN; + this.onopen?.({ type: 'open' }); + this.onmessage?.({ data: HEALTH_CHECK_PAYLOAD }); + }); + } + + send() {} + + close() { + this.readyState = MockWebSocket.CLOSED; + this.onclose?.({ code: 1000, reason: '', wasClean: true }); + } +} + +// A test-only WebSocket that fails to connect — mirrors a real WebSocket +// against an unreachable host (fires an error, then a code 1006 close). +class FailingWebSocket { + static CONNECTING = 0; + static OPEN = 1; + static CLOSING = 2; + static CLOSED = 3; + + CONNECTING = 0; + OPEN = 1; + CLOSING = 2; + CLOSED = 3; + + onopen = null; + onclose = null; + onerror = null; + onmessage = null; + + constructor(url) { + this.url = url; + this.readyState = FailingWebSocket.CONNECTING; + + queueMicrotask(() => { + this.readyState = FailingWebSocket.CLOSED; + this.onerror?.({ type: 'error' }); + this.onclose?.({ code: 1006, reason: '', wasClean: false }); + }); + } + + send() {} + + close() { + this.readyState = FailingWebSocket.CLOSED; + } +} describe('connection', function () { - const wsBaseURL = 'http://localhost:9999'; - const tokenManager = new TokenManager('secret'); + const wsBaseURL = 'ws://localhost:9999'; + const tokenManager = new TokenManager(); + tokenManager.token = 't.oke.n'; const user = { name: 'amin', id: 'amin' }; const newStreamChat = () => { - const client = new StreamChat('key'); + const client = new StreamChat('key', { WebSocketImpl: MockWebSocket }); client.wsBaseURL = wsBaseURL; client.tokenManager = tokenManager; client._user = user; @@ -28,20 +105,11 @@ describe('connection', function () { return client; }; - // dummy server to use instead of actual Stream API - const wss = new WsServer({ port: 9999 }); - wss.on('connection', (ws) => - ws.send( - '{"type":"health.check","connection_id":"61112366-0a15-3891-0000-000000000009","cid":"*","me":{"id":"amin","role":"user","created_at":"2021-07-27T13:18:23.293696Z","updated_at":"2021-07-27T13:20:08.047284Z","last_active":"2021-08-11T10:42:44.213510048Z","banned":false,"online":true,"invisible":false,"devices":[],"mutes":[],"channel_mutes":[],"unread_count":98,"total_unread_count":98,"unread_channels":18,"language":"","image":"https://cdn.fakercloud.com/avatars/Shriiiiimp_128.jpg","name":"amin"},"created_at":"2021-08-11T10:42:44.222203145Z"}', - ), - ); - - afterAll(() => wss.close()); - describe('Connection tokenProvider', () => { it('should handle token provider rejection ', async () => { const client = new StreamChat('apiKey', { allowServerSideConnect: true, + WebSocketImpl: MockWebSocket, }); client.defaultWSTimeout = 20; const tokenProvider = () => Promise.reject(new Error('network failure')); @@ -110,7 +178,7 @@ describe('connection', function () { it('should set isResolved', async () => { const c = new StableWSConnection({ client: newStreamChat() }); expect(c.isResolved).to.be.false; - const res = await c.connect(); + await c.connect(); expect(c.isResolved).to.be.true; }); @@ -180,6 +248,7 @@ describe('connection', function () { it('should set and unset the flag correctly without opening WS', async () => { const client = newStreamChat(); + client.options.WebSocketImpl = FailingWebSocket; client.wsBaseURL = 'https://stream-dummy-test.com'; const c = new StableWSConnection({ client }); @@ -214,6 +283,7 @@ describe('connection', function () { const client = new StreamChat('apiKey', { allowServerSideConnect: true, baseURL: 'http://localhost:1111', // invalid base url + WebSocketImpl: FailingWebSocket, }); client.defaultWSTimeout = 2000; @@ -225,7 +295,8 @@ describe('connection', function () { it('should retry until connection is established', async function () { const client = new StreamChat('apiKey', { allowServerSideConnect: true, - baseURL: 'http://localhost:1111', // invalid base url + baseURL: 'http://localhost:1111', + WebSocketImpl: FailingWebSocket, }); client.defaultWSTimeout = 5000; @@ -234,7 +305,8 @@ describe('connection', function () { expect(health.type).to.be.equal('health.check'); }), sleep(1000).then(() => { - // set the correct url after connectUser failed and is trying to connect + // swap in the healthy mock so the retrying connect will succeed + client.options.WebSocketImpl = MockWebSocket; client.setBaseURL(wsBaseURL); client.wsConnection.wsBaseURL = client.wsBaseURL; }), diff --git a/test/unit/signing.test.js b/test/unit/signing.test.js index 17300579a..10780f5c3 100644 --- a/test/unit/signing.test.js +++ b/test/unit/signing.test.js @@ -1,30 +1,25 @@ -import { CheckSignature } from '../../src'; +import { UserFromToken } from '../../src'; -import { describe, it, expect } from 'vitest'; - -const MOCK_SECRET = 'porewqKAFDSAKZssecretsercretfads'; -const MOCK_TEXT = 'text'; -const MOCK_JSON_BODY = { a: 1 }; -const MOCK_TEXT_SHA256 = - 'd0b770e93a56adc3ee9ac5734533cc0acd71eea8e5e8204a28042ca0f60de1f3'; -const MOCK_JSON_SHA256 = - 'e527a6ad4993a4c9a30680c8be4b3eda1c36ab104f1f7d39c744bd27016a9624'; +import { describe, expect, it } from 'vitest'; describe('Signing', () => { - describe('CheckSignature', () => { - it('validates correct text body and signature', () => { - const rawBody = Buffer.from(MOCK_TEXT); - expect(CheckSignature(rawBody, MOCK_SECRET, MOCK_TEXT_SHA256)).to.be.true; + describe('UserFromToken', () => { + it('extracts the user_id from a valid JWT payload', () => { + // payload: {"user_id":"amin"} + const token = '_.eyJ1c2VyX2lkIjoiYW1pbiJ9._'; + expect(UserFromToken(token)).to.equal('amin'); }); - it('validates correct json body and signature', () => { - const rawBody = Buffer.from(JSON.stringify(MOCK_JSON_BODY)); - expect(CheckSignature(rawBody, MOCK_SECRET, MOCK_JSON_SHA256)).to.be.true; + it('returns an empty string for a token that is not three fragments', () => { + expect(UserFromToken('not-a-token')).to.equal(''); + expect(UserFromToken('only.two')).to.equal(''); + expect(UserFromToken('too.many.fragments.here')).to.equal(''); }); - it('refutes incorrect json body', () => { - const rawBody = Buffer.from(JSON.stringify({ ...MOCK_JSON_BODY, b: 2 })); - expect(CheckSignature(rawBody, MOCK_SECRET, MOCK_JSON_SHA256)).to.be.false; + it('returns undefined when the payload has no user_id', () => { + // payload: {"foo":"bar"} + const token = 'eyJhbGciOiJIUzI1NiJ9.eyJmb28iOiJiYXIifQ.sig'; + expect(UserFromToken(token)).to.be.undefined; }); }); }); diff --git a/tsconfig.json b/tsconfig.json index 46b3ff41e..781465573 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -26,7 +26,8 @@ "lib": ["ES2022", "DOM", "ES2022.Error"], "moduleResolution": "bundler", "module": "Preserve", - "target": "ES2020" + "target": "ES2020", + "types": ["node"] }, "include": ["./src/**/*"] } diff --git a/yarn.lock b/yarn.lock index 67774b5e7..ccc713b99 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1563,29 +1563,12 @@ __metadata: languageName: node linkType: hard -"@types/jsonwebtoken@npm:^9.0.8": - version: 9.0.8 - resolution: "@types/jsonwebtoken@npm:9.0.8" - dependencies: - "@types/ms": "npm:*" - "@types/node": "npm:*" - checksum: 10c0/dd3ccea05115ad9c4458d0c2a487179b1abf8886eaa8cc77d64031a8ef47abd3ff038cdde26e76893372238c8612e4f5fd578b3400e525b65c0e8d3abefe5282 - languageName: node - linkType: hard - -"@types/ms@npm:*": - version: 2.1.0 - resolution: "@types/ms@npm:2.1.0" - checksum: 10c0/5ce692ffe1549e1b827d99ef8ff71187457e0eb44adbae38fdf7b9a74bae8d20642ee963c14516db1d35fa2652e65f47680fdf679dcbde52bbfadd021f497225 - languageName: node - linkType: hard - -"@types/node@npm:*, @types/node@npm:^22.19.19": - version: 22.19.19 - resolution: "@types/node@npm:22.19.19" +"@types/node@npm:^22": + version: 22.20.1 + resolution: "@types/node@npm:22.20.1" dependencies: undici-types: "npm:~6.21.0" - checksum: 10c0/402e0f088c94cabda3cd721546bd8e4e75e098e0b342f6e03b90ca1e19c28986f9650112c64fcfd09fc8cebc0f8b20291a513153e90489331cf666e1e5503e16 + checksum: 10c0/f2ba54d3d1fb92e1c57c78d32c3a17655b1e87363b707136f55c422b4838d4054901ce5d27f75bb0e5ecb7ebfee3804e0987822d22b473473008091857a09353 languageName: node linkType: hard @@ -1605,15 +1588,6 @@ __metadata: languageName: node linkType: hard -"@types/ws@npm:^8.18.1": - version: 8.18.1 - resolution: "@types/ws@npm:8.18.1" - dependencies: - "@types/node": "npm:*" - checksum: 10c0/61aff1129143fcc4312f083bc9e9e168aa3026b7dd6e70796276dcfb2c8211c4292603f9c4864fae702f2ed86e4abd4d38aa421831c2fd7f856c931a481afbab - languageName: node - linkType: hard - "@typescript-eslint/eslint-plugin@npm:8.59.4": version: 8.59.4 resolution: "@typescript-eslint/eslint-plugin@npm:8.59.4" @@ -2200,13 +2174,6 @@ __metadata: languageName: node linkType: hard -"base64-js@npm:^1.5.1": - version: 1.5.1 - resolution: "base64-js@npm:1.5.1" - checksum: 10c0/f23823513b63173a001030fae4f2dabe283b99a9d324ade3ad3d148e218134676f1ee8568c877cd79ec1c53158dcf2d2ba527a97c606618928ba99dd930102bf - languageName: node - linkType: hard - "before-after-hook@npm:^4.0.0": version: 4.0.0 resolution: "before-after-hook@npm:4.0.0" @@ -2278,13 +2245,6 @@ __metadata: languageName: node linkType: hard -"buffer-equal-constant-time@npm:^1.0.1": - version: 1.0.1 - resolution: "buffer-equal-constant-time@npm:1.0.1" - checksum: 10c0/fb2294e64d23c573d0dd1f1e7a466c3e978fe94a4e0f8183937912ca374619773bef8e2aceb854129d2efecbbc515bbd0cc78d2734a3e3031edb0888531bbc8e - languageName: node - linkType: hard - "cacache@npm:^20.0.0, cacache@npm:^20.0.1, cacache@npm:^20.0.3": version: 20.0.3 resolution: "cacache@npm:20.0.3" @@ -2924,15 +2884,6 @@ __metadata: languageName: node linkType: hard -"ecdsa-sig-formatter@npm:1.0.11": - version: 1.0.11 - resolution: "ecdsa-sig-formatter@npm:1.0.11" - dependencies: - safe-buffer: "npm:^5.0.1" - checksum: 10c0/ebfbf19d4b8be938f4dd4a83b8788385da353d63307ede301a9252f9f7f88672e76f2191618fd8edfc2f24679236064176fab0b78131b161ee73daa37125408c - languageName: node - linkType: hard - "emoji-regex@npm:^10.3.0": version: 10.3.0 resolution: "emoji-regex@npm:10.3.0" @@ -4722,15 +4673,6 @@ __metadata: languageName: node linkType: hard -"isomorphic-ws@npm:^5.0.0": - version: 5.0.0 - resolution: "isomorphic-ws@npm:5.0.0" - peerDependencies: - ws: "*" - checksum: 10c0/a058ac8b5e6efe9e46252cb0bc67fd325005d7216451d1a51238bc62d7da8486f828ef017df54ddf742e0fffcbe4b1bcc2a66cc115b027ed0180334cd18df252 - languageName: node - linkType: hard - "issue-parser@npm:^7.0.0": version: 7.0.1 resolution: "issue-parser@npm:7.0.1" @@ -4914,24 +4856,6 @@ __metadata: languageName: node linkType: hard -"jsonwebtoken@npm:^9.0.3": - version: 9.0.3 - resolution: "jsonwebtoken@npm:9.0.3" - dependencies: - jws: "npm:^4.0.1" - lodash.includes: "npm:^4.3.0" - lodash.isboolean: "npm:^3.0.3" - lodash.isinteger: "npm:^4.0.4" - lodash.isnumber: "npm:^3.0.3" - lodash.isplainobject: "npm:^4.0.6" - lodash.isstring: "npm:^4.0.1" - lodash.once: "npm:^4.0.0" - ms: "npm:^2.1.1" - semver: "npm:^7.5.4" - checksum: 10c0/6ca7f1e54886ea3bde7146a5a22b53847c46e25453c7f7307a69818b9a6ad48c390b2e59d5690fcfd03c529b01960060cc4bb0c686991d6edae2285dfd30f4ba - languageName: node - linkType: hard - "just-diff-apply@npm:^5.2.0": version: 5.5.0 resolution: "just-diff-apply@npm:5.5.0" @@ -4953,27 +4877,6 @@ __metadata: languageName: node linkType: hard -"jwa@npm:^2.0.1": - version: 2.0.1 - resolution: "jwa@npm:2.0.1" - dependencies: - buffer-equal-constant-time: "npm:^1.0.1" - ecdsa-sig-formatter: "npm:1.0.11" - safe-buffer: "npm:^5.0.1" - checksum: 10c0/ab3ebc6598e10dc11419d4ed675c9ca714a387481466b10e8a6f3f65d8d9c9237e2826f2505280a739cf4cbcf511cb288eeec22b5c9c63286fc5a2e4f97e78cf - languageName: node - linkType: hard - -"jws@npm:^4.0.1": - version: 4.0.1 - resolution: "jws@npm:4.0.1" - dependencies: - jwa: "npm:^2.0.1" - safe-buffer: "npm:^5.0.1" - checksum: 10c0/6be1ed93023aef570ccc5ea8d162b065840f3ef12f0d1bb3114cade844de7a357d5dc558201d9a65101e70885a6fa56b17462f520e6b0d426195510618a154d0 - languageName: node - linkType: hard - "keyv@npm:^4.5.4": version: 4.5.4 resolution: "keyv@npm:4.5.4" @@ -5342,34 +5245,6 @@ __metadata: languageName: node linkType: hard -"lodash.includes@npm:^4.3.0": - version: 4.3.0 - resolution: "lodash.includes@npm:4.3.0" - checksum: 10c0/7ca498b9b75bf602d04e48c0adb842dfc7d90f77bcb2a91a2b2be34a723ad24bc1c8b3683ec6b2552a90f216c723cdea530ddb11a3320e08fa38265703978f4b - languageName: node - linkType: hard - -"lodash.isboolean@npm:^3.0.3": - version: 3.0.3 - resolution: "lodash.isboolean@npm:3.0.3" - checksum: 10c0/0aac604c1ef7e72f9a6b798e5b676606042401dd58e49f051df3cc1e3adb497b3d7695635a5cbec4ae5f66456b951fdabe7d6b387055f13267cde521f10ec7f7 - languageName: node - linkType: hard - -"lodash.isinteger@npm:^4.0.4": - version: 4.0.4 - resolution: "lodash.isinteger@npm:4.0.4" - checksum: 10c0/4c3e023a2373bf65bf366d3b8605b97ec830bca702a926939bcaa53f8e02789b6a176e7f166b082f9365bfec4121bfeb52e86e9040cb8d450e64c858583f61b7 - languageName: node - linkType: hard - -"lodash.isnumber@npm:^3.0.3": - version: 3.0.3 - resolution: "lodash.isnumber@npm:3.0.3" - checksum: 10c0/2d01530513a1ee4f72dd79528444db4e6360588adcb0e2ff663db2b3f642d4bb3d687051ae1115751ca9082db4fdef675160071226ca6bbf5f0c123dbf0aa12d - languageName: node - linkType: hard - "lodash.isplainobject@npm:^4.0.6": version: 4.0.6 resolution: "lodash.isplainobject@npm:4.0.6" @@ -5391,13 +5266,6 @@ __metadata: languageName: node linkType: hard -"lodash.once@npm:^4.0.0": - version: 4.1.1 - resolution: "lodash.once@npm:4.1.1" - checksum: 10c0/46a9a0a66c45dd812fcc016e46605d85ad599fe87d71a02f6736220554b52ffbe82e79a483ad40f52a8a95755b0d1077fba259da8bfb6694a7abbf4a48f1fc04 - languageName: node - linkType: hard - "lodash.uniqby@npm:^4.7.0": version: 4.7.0 resolution: "lodash.uniqby@npm:4.7.0" @@ -6947,13 +6815,6 @@ __metadata: languageName: node linkType: hard -"safe-buffer@npm:^5.0.1": - version: 5.2.1 - resolution: "safe-buffer@npm:5.2.1" - checksum: 10c0/6501914237c0a86e9675d4e51d89ca3c21ffd6a31642efeba25ad65720bce6921c9e7e974e5be91a786b25aa058b5303285d3c15dbabf983a919f5f630d349f3 - languageName: node - linkType: hard - "safe-buffer@npm:~5.1.0, safe-buffer@npm:~5.1.1": version: 5.1.2 resolution: "safe-buffer@npm:5.1.2" @@ -7043,7 +6904,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.1.1, semver@npm:^7.1.2, semver@npm:^7.3.2, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:^7.7.2, semver@npm:^7.7.3": +"semver@npm:^7.1.1, semver@npm:^7.1.2, semver@npm:^7.3.2, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.6.0, semver@npm:^7.7.2, semver@npm:^7.7.3": version: 7.7.3 resolution: "semver@npm:7.7.3" bin: @@ -7410,13 +7271,10 @@ __metadata: "@semantic-release/changelog": "npm:^6.0.3" "@semantic-release/git": "npm:^10.0.1" "@stream-io/logger": "npm:^2.0.0" - "@types/jsonwebtoken": "npm:^9.0.8" - "@types/node": "npm:^22.19.19" + "@types/node": "npm:^22" "@types/sinon": "npm:^10.0.6" - "@types/ws": "npm:^8.18.1" "@vitest/coverage-v8": "npm:^4.1.7" axios: "npm:^1.16.1" - base64-js: "npm:^1.5.1" concurrently: "npm:^9.2.1" conventional-changelog-conventionalcommits: "npm:^9.3.1" dotenv: "npm:^17.4.2" @@ -7425,11 +7283,8 @@ __metadata: eslint-plugin-import: "npm:^2.32.0" eslint-plugin-jsdoc: "npm:^63.0.7" eslint-plugin-unused-imports: "npm:^4.4.1" - form-data: "npm:^4.0.5" globals: "npm:^17.6.0" husky: "npm:^9.1.7" - isomorphic-ws: "npm:^5.0.0" - jsonwebtoken: "npm:^9.0.3" linkifyjs: "npm:^4.3.3" lint-staged: "npm:^17.0.5" prettier: "npm:^3.8.3" @@ -7438,7 +7293,6 @@ __metadata: typescript: "npm:^6.0.3" typescript-eslint: "npm:^8.59.4" vitest: "npm:^4.1.7" - ws: "npm:^8.20.1" dependenciesMeta: esbuild: built: true @@ -8444,21 +8298,6 @@ __metadata: languageName: node linkType: hard -"ws@npm:^8.20.1": - version: 8.20.1 - resolution: "ws@npm:8.20.1" - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: ">=5.0.2" - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - checksum: 10c0/ce162433218399cdedeb76fd33363d4d86a7d910058d4e3c679dce08cea65d6da6b39f11baa4d7808d024cf46ed88f6a05c17611621aaad8fc5e62edacc30c5d - languageName: node - linkType: hard - "xtend@npm:~4.0.1": version: 4.0.2 resolution: "xtend@npm:4.0.2"