stream-chat 9.43.2 → 9.44.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1246,6 +1246,38 @@ export declare class StreamChat {
1246
1246
  * @returns {boolean}
1247
1247
  */
1248
1248
  verifyWebhook(requestBody: string | Buffer, xSignature: string): boolean;
1249
+ /**
1250
+ * Verify and parse an HTTP webhook event.
1251
+ *
1252
+ * Decompresses `rawBody` when gzipped (detected from the body bytes),
1253
+ * verifies the `X-Signature` header against the app's API secret, and
1254
+ * returns the parsed `Event`. Works whether or not Stream is currently
1255
+ * compressing payloads for this app, and stays correct behind
1256
+ * middleware that auto-decompresses the request.
1257
+ *
1258
+ * @param rawBody Raw HTTP request body bytes Stream signed
1259
+ * @param signature Value of the `X-Signature` header
1260
+ * @throws {InvalidWebhookError} When the signature does not match or
1261
+ * the gzip envelope is malformed.
1262
+ */
1263
+ verifyAndParseWebhook(rawBody: string | Buffer, signature: string): Event;
1264
+ /**
1265
+ * Parse an SQS firehose event: decodes the message `Body` (base64 +
1266
+ * optional gzip) and returns the parsed `Event`. No HMAC verification
1267
+ * (Stream does not sign SQS bodies).
1268
+ *
1269
+ * @param messageBody SQS message `Body` string
1270
+ * @throws {InvalidWebhookError} When the base64 / gzip envelope is malformed.
1271
+ */
1272
+ parseSqs(messageBody: string): Event;
1273
+ /**
1274
+ * Parse an SNS-delivered event (unwraps envelope JSON when needed, then
1275
+ * same decode path as SQS). No HMAC verification.
1276
+ *
1277
+ * @param notificationBody Raw SNS POST body or pre-extracted `Message` string
1278
+ * @throws {InvalidWebhookError} When the envelope cannot be decoded.
1279
+ */
1280
+ parseSns(notificationBody: string): Event;
1249
1281
  /** getPermission - gets the definition for a permission
1250
1282
  *
1251
1283
  * @param {string} name
@@ -1,4 +1,4 @@
1
- import type { AppSettingsAPIResponse, ChannelAPIResponse, ChannelFilters, ChannelMemberResponse, ChannelResponse, ChannelSort, DraftResponse, LocalMessage, MessageResponse, PollResponse, ReactionFilters, ReactionResponse, ReactionSort, ReadResponse } from '../types';
1
+ import type { AppSettingsAPIResponse, ChannelAPIResponse, ChannelFilters, ChannelMemberResponse, ChannelOptions, ChannelResponse, ChannelSort, DraftResponse, LocalMessage, MessageResponse, PollResponse, ReactionFilters, ReactionResponse, ReactionSort, ReadResponse } from '../types';
2
2
  import type { Channel } from '../channel';
3
3
  import type { StreamChat } from '../client';
4
4
  export type PrepareBatchDBQueries = [string] | [string, Array<unknown> | Array<Array<unknown>>];
@@ -21,6 +21,8 @@ export type DBUpsertCidsForQueryType = {
21
21
  cids: string[];
22
22
  /** Optional filters for the channels. */
23
23
  filters?: ChannelFilters;
24
+ /** Optional full query options for the channels. */
25
+ options?: ChannelOptions;
24
26
  /** Whether to immediately execute the operation. */
25
27
  execute?: boolean;
26
28
  /** Optional sorting applied to the channels. */
@@ -145,6 +147,8 @@ export type DBGetChannelsForQueryType = {
145
147
  userId: string;
146
148
  /** Optional filters for channels. */
147
149
  filters?: ChannelFilters;
150
+ /** Optional full query options for channels. */
151
+ options?: ChannelOptions;
148
152
  /** Optional sorting for the channels. */
149
153
  sort?: ChannelSort;
150
154
  };
@@ -1,5 +1,5 @@
1
1
  import jwt from 'jsonwebtoken';
2
- import type { UR } from './types';
2
+ import type { Event, UR } from './types';
3
3
  /**
4
4
  * Creates the JWT token that can be used for a UserSession
5
5
  * @method JWTUserToken
@@ -21,10 +21,104 @@ export declare function UserFromToken(token: string): string;
21
21
  */
22
22
  export declare function DevToken(userId: string): string;
23
23
  /**
24
+ * Constant-time HMAC-SHA256 verification of `signature` against the
25
+ * digest of `body` using `secret` as the key. The signature is always
26
+ * computed over the **uncompressed** JSON bytes, so callers that
27
+ * decoded a gzipped or base64-wrapped payload must pass the inflated
28
+ * bytes here.
24
29
  *
25
- * @param {string | Buffer} body the signed message
26
- * @param {string} secret the shared secret used to generate the signature (Stream API secret)
27
- * @param {string} signature the signature to validate
28
- * @return {boolean}
30
+ * The legacy `client.verifyWebhook` helper wraps this function, so
31
+ * callers that have already migrated to `verifyAndParseWebhook`,
32
+ * `parseSqs`, or `parseSns` rarely need to invoke this
33
+ * directly.
34
+ */
35
+ export declare function verifySignature(body: string | Buffer, signature: string, secret: string): boolean;
36
+ /**
37
+ * @deprecated Use {@link verifySignature} - same logic, parameters
38
+ * reordered to match the cross-SDK contract
39
+ * (`verifySignature(body, signature, secret)`).
29
40
  */
30
41
  export declare function CheckSignature(body: string | Buffer, secret: string, signature: string): boolean;
42
+ /**
43
+ * Canonical failure-mode messages for {@link InvalidWebhookError}.
44
+ *
45
+ * Customers that prefer exact-match filtering (security logging, retry
46
+ * policy) over substring matches can compare `err.message` to these
47
+ * constants instead of pattern-matching free-form text.
48
+ */
49
+ export declare const InvalidWebhookErrorMessages: {
50
+ readonly signatureMismatch: "signature mismatch";
51
+ readonly invalidBase64: "invalid base64 encoding";
52
+ readonly gzipFailed: "gzip decompression failed";
53
+ readonly invalidJson: "invalid JSON payload";
54
+ };
55
+ /**
56
+ * Thrown by {@link verifyAndParseWebhook} when the supplied `x-signature` does not
57
+ * match the HMAC of the uncompressed payload, and by all webhook helpers (including
58
+ * {@link parseSqs} / {@link parseSns}) when a gzip / base64 / JSON envelope is malformed.
59
+ *
60
+ * The message identifies which failure mode fired. See
61
+ * {@link InvalidWebhookErrorMessages} for the canonical strings.
62
+ */
63
+ export declare class InvalidWebhookError extends Error {
64
+ name: string;
65
+ constructor(message?: string);
66
+ }
67
+ /**
68
+ * Returns `body` as a `Buffer`, gzip-decompressed when its first two
69
+ * bytes match the gzip magic (`1f 8b`, per RFC 1952). When the body is
70
+ * plain JSON (no compression, or middleware already decompressed), the
71
+ * bytes are returned unchanged.
72
+ *
73
+ * Magic-byte detection (rather than relying on a header) keeps the
74
+ * same handler correct when middleware - Express, Next.js, AWS Lambda
75
+ * - auto-decompresses the request before your code sees it.
76
+ */
77
+ export declare function gunzipPayload(rawBody: string | Buffer): Buffer;
78
+ /**
79
+ * Reverses the SQS firehose envelope: the message `Body` is
80
+ * base64-decoded, then the result is gzip-decompressed when it begins
81
+ * with the gzip magic. Returns the raw JSON `Buffer` Stream signed.
82
+ *
83
+ * SQS bodies are always base64-encoded so they remain valid UTF-8 over
84
+ * the queue. The same call works whether or not Stream is currently
85
+ * compressing payloads for this app.
86
+ */
87
+ export declare function decodeSqsPayload(body: string): Buffer;
88
+ /**
89
+ * Reverses an SNS HTTP notification envelope. When `notificationBody`
90
+ * is a JSON envelope (`{"Type":"Notification","Message":"..."}`), the
91
+ * inner `Message` field is extracted and run through the SQS pipeline
92
+ * (base64-decode, then gzip-if-magic). When the input is not a JSON
93
+ * envelope it is treated as the already-extracted `Message` string,
94
+ * so call sites that pre-unwrap continue to work.
95
+ */
96
+ export declare function decodeSnsPayload(notificationBody: string): Buffer;
97
+ /**
98
+ * Parse a JSON-encoded webhook event into a typed {@link Event}. New
99
+ * event types Stream introduces still parse successfully - the runtime
100
+ * shape is the JSON Stream sent and the `type` field stays preserved.
101
+ */
102
+ export declare function parseEvent(payload: Buffer | string): Event;
103
+ /**
104
+ * Decompress (when gzipped), verify the HMAC `signature`, and return
105
+ * the parsed {@link Event}.
106
+ *
107
+ * @param rawBody Raw HTTP request body bytes Stream signed
108
+ * @param signature Value of the `X-Signature` header
109
+ * @param secret Your app's API secret
110
+ * @throws {InvalidWebhookError} When the signature does not match or
111
+ * the gzip envelope is malformed.
112
+ */
113
+ export declare function verifyAndParseWebhook(rawBody: string | Buffer, signature: string, secret: string): Event;
114
+ /**
115
+ * Decode the SQS message `Body` (base64, then gzip-if-magic) and return
116
+ * the parsed {@link Event}. Stream does not attach an application-level HMAC
117
+ * to SQS deliveries — use {@link verifyAndParseWebhook} for HTTP webhooks.
118
+ */
119
+ export declare function parseSqs(messageBody: string): Event;
120
+ /**
121
+ * Decode an SNS notification (unwrap the JSON envelope when needed; same
122
+ * inner format as SQS). No application-level HMAC verification.
123
+ */
124
+ export declare function parseSns(notificationBody: string): Event;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stream-chat",
3
- "version": "9.43.2",
3
+ "version": "9.44.1",
4
4
  "description": "JS SDK for the Stream Chat API",
5
5
  "homepage": "https://getstream.io/chat/",
6
6
  "author": {
@@ -31,6 +31,7 @@
31
31
  "browser": {
32
32
  "https": false,
33
33
  "crypto": false,
34
+ "zlib": false,
34
35
  "jsonwebtoken": false,
35
36
  "ws": false
36
37
  },
package/src/channel.ts CHANGED
@@ -1438,7 +1438,9 @@ export class Channel {
1438
1438
  }
1439
1439
 
1440
1440
  // FIXME: see #1265, adjust and count new messages even when the channel is muted
1441
- if (this.muteStatus().muted) return false;
1441
+ // Read mute state directly from the client to avoid _checkInitialized() this method
1442
+ // is invoked from _handleChannelEvent (e.g. message.new) before .watch() resolves.
1443
+ if (this.getClient()._muteStatus(this.cid).muted) return false;
1442
1444
 
1443
1445
  return true;
1444
1446
  }
@@ -231,13 +231,14 @@ export class ChannelManager extends WithSubscriptions {
231
231
  });
232
232
  const {
233
233
  channels,
234
- pagination: { filters, sort },
234
+ pagination: { filters, options, sort },
235
235
  } = this.state.getLatestValue();
236
236
  this.client.offlineDb?.executeQuerySafely(
237
237
  (db) =>
238
238
  db.upsertCidsForQuery({
239
239
  cids: channels.map((channel) => channel.cid),
240
240
  filters,
241
+ options,
241
242
  sort,
242
243
  }),
243
244
  { method: 'upsertCidsForQuery' },
@@ -304,6 +305,7 @@ export class ChannelManager extends WithSubscriptions {
304
305
  db.upsertCidsForQuery({
305
306
  cids: channels.map((channel) => channel.cid),
306
307
  filters: pagination.filters,
308
+ options,
307
309
  sort: pagination.sort,
308
310
  }),
309
311
  { method: 'upsertCidsForQuery' },
@@ -381,6 +383,7 @@ export class ChannelManager extends WithSubscriptions {
381
383
  const channelsFromDB = await this.client.offlineDb.getChannelsForQuery({
382
384
  userId: this.client.user.id,
383
385
  filters,
386
+ options,
384
387
  sort,
385
388
  });
386
389
 
package/src/client.ts CHANGED
@@ -9,7 +9,15 @@ import { Channel } from './channel';
9
9
  import { ClientState } from './client_state';
10
10
  import { StableWSConnection } from './connection';
11
11
  import { UploadManager } from './uploadManager';
12
- import { CheckSignature, DevToken, JWTUserToken } from './signing';
12
+ import {
13
+ DevToken,
14
+ InvalidWebhookError,
15
+ JWTUserToken,
16
+ parseSns as parseSnsHelper,
17
+ parseSqs as parseSqsHelper,
18
+ verifyAndParseWebhook as verifyAndParseWebhookHelper,
19
+ verifySignature,
20
+ } from './signing';
13
21
  import { TokenManager } from './token_manager';
14
22
  import { WSConnectionFallback } from './connection_fallback';
15
23
  import { Campaign } from './campaign';
@@ -3639,7 +3647,53 @@ export class StreamChat {
3639
3647
  * @returns {boolean}
3640
3648
  */
3641
3649
  verifyWebhook(requestBody: string | Buffer, xSignature: string) {
3642
- return !!this.secret && CheckSignature(requestBody, this.secret, xSignature);
3650
+ return !!this.secret && verifySignature(requestBody, xSignature, this.secret);
3651
+ }
3652
+
3653
+ /**
3654
+ * Verify and parse an HTTP webhook event.
3655
+ *
3656
+ * Decompresses `rawBody` when gzipped (detected from the body bytes),
3657
+ * verifies the `X-Signature` header against the app's API secret, and
3658
+ * returns the parsed `Event`. Works whether or not Stream is currently
3659
+ * compressing payloads for this app, and stays correct behind
3660
+ * middleware that auto-decompresses the request.
3661
+ *
3662
+ * @param rawBody Raw HTTP request body bytes Stream signed
3663
+ * @param signature Value of the `X-Signature` header
3664
+ * @throws {InvalidWebhookError} When the signature does not match or
3665
+ * the gzip envelope is malformed.
3666
+ */
3667
+ verifyAndParseWebhook(rawBody: string | Buffer, signature: string) {
3668
+ if (!this.secret) {
3669
+ throw new InvalidWebhookError(
3670
+ 'cannot verify webhook signature without an API secret on the client',
3671
+ );
3672
+ }
3673
+ return verifyAndParseWebhookHelper(rawBody, signature, this.secret);
3674
+ }
3675
+
3676
+ /**
3677
+ * Parse an SQS firehose event: decodes the message `Body` (base64 +
3678
+ * optional gzip) and returns the parsed `Event`. No HMAC verification
3679
+ * (Stream does not sign SQS bodies).
3680
+ *
3681
+ * @param messageBody SQS message `Body` string
3682
+ * @throws {InvalidWebhookError} When the base64 / gzip envelope is malformed.
3683
+ */
3684
+ parseSqs(messageBody: string) {
3685
+ return parseSqsHelper(messageBody);
3686
+ }
3687
+
3688
+ /**
3689
+ * Parse an SNS-delivered event (unwraps envelope JSON when needed, then
3690
+ * same decode path as SQS). No HMAC verification.
3691
+ *
3692
+ * @param notificationBody Raw SNS POST body or pre-extracted `Message` string
3693
+ * @throws {InvalidWebhookError} When the envelope cannot be decoded.
3694
+ */
3695
+ parseSns(notificationBody: string) {
3696
+ return parseSnsHelper(notificationBody);
3643
3697
  }
3644
3698
 
3645
3699
  /** getPermission - gets the definition for a permission
@@ -624,6 +624,7 @@ export class MessageComposer extends WithSubscriptions {
624
624
  draft.channel_cid !== this.channel.cid
625
625
  )
626
626
  return;
627
+ if (this.editedMessage) return;
627
628
  this.initState({ composition: draft });
628
629
  }).unsubscribe;
629
630
 
@@ -637,6 +638,7 @@ export class MessageComposer extends WithSubscriptions {
637
638
  ) {
638
639
  return;
639
640
  }
641
+ if (this.editedMessage) return;
640
642
 
641
643
  this.logDraftUpdateTimestamp();
642
644
 
@@ -3,6 +3,7 @@ import type {
3
3
  ChannelAPIResponse,
4
4
  ChannelFilters,
5
5
  ChannelMemberResponse,
6
+ ChannelOptions,
6
7
  ChannelResponse,
7
8
  ChannelSort,
8
9
  DraftResponse,
@@ -41,6 +42,8 @@ export type DBUpsertCidsForQueryType = {
41
42
  cids: string[];
42
43
  /** Optional filters for the channels. */
43
44
  filters?: ChannelFilters;
45
+ /** Optional full query options for the channels. */
46
+ options?: ChannelOptions;
44
47
  /** Whether to immediately execute the operation. */
45
48
  execute?: boolean;
46
49
  /** Optional sorting applied to the channels. */
@@ -177,6 +180,8 @@ export type DBGetChannelsForQueryType = {
177
180
  userId: string;
178
181
  /** Optional filters for channels. */
179
182
  filters?: ChannelFilters;
183
+ /** Optional full query options for channels. */
184
+ options?: ChannelOptions;
180
185
  /** Optional sorting for the channels. */
181
186
  sort?: ChannelSort;
182
187
  };
package/src/signing.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import jwt from 'jsonwebtoken';
2
2
  import crypto from 'crypto';
3
+ import zlib from 'zlib';
3
4
  import { decodeBase64, encodeBase64 } from './base64';
4
- import type { UR } from './types';
5
+ import type { Event, UR } from './types';
5
6
 
6
7
  /**
7
8
  * Creates the JWT token that can be used for a UserSession
@@ -84,19 +85,206 @@ export function DevToken(userId: string) {
84
85
  }
85
86
 
86
87
  /**
88
+ * Constant-time HMAC-SHA256 verification of `signature` against the
89
+ * digest of `body` using `secret` as the key. The signature is always
90
+ * computed over the **uncompressed** JSON bytes, so callers that
91
+ * decoded a gzipped or base64-wrapped payload must pass the inflated
92
+ * bytes here.
87
93
  *
88
- * @param {string | Buffer} body the signed message
89
- * @param {string} secret the shared secret used to generate the signature (Stream API secret)
90
- * @param {string} signature the signature to validate
91
- * @return {boolean}
94
+ * The legacy `client.verifyWebhook` helper wraps this function, so
95
+ * callers that have already migrated to `verifyAndParseWebhook`,
96
+ * `parseSqs`, or `parseSns` rarely need to invoke this
97
+ * directly.
92
98
  */
93
- export function CheckSignature(body: string | Buffer, secret: string, signature: string) {
99
+ export function verifySignature(
100
+ body: string | Buffer,
101
+ signature: string,
102
+ secret: string,
103
+ ): boolean {
94
104
  const key = Buffer.from(secret, 'utf8');
95
105
  const hash = crypto.createHmac('sha256', key).update(body).digest('hex');
96
-
97
106
  try {
98
107
  return crypto.timingSafeEqual(Buffer.from(hash), Buffer.from(signature));
99
108
  } catch {
100
109
  return false;
101
110
  }
102
111
  }
112
+
113
+ /**
114
+ * @deprecated Use {@link verifySignature} - same logic, parameters
115
+ * reordered to match the cross-SDK contract
116
+ * (`verifySignature(body, signature, secret)`).
117
+ */
118
+ export function CheckSignature(body: string | Buffer, secret: string, signature: string) {
119
+ return verifySignature(body, signature, secret);
120
+ }
121
+
122
+ /**
123
+ * Canonical failure-mode messages for {@link InvalidWebhookError}.
124
+ *
125
+ * Customers that prefer exact-match filtering (security logging, retry
126
+ * policy) over substring matches can compare `err.message` to these
127
+ * constants instead of pattern-matching free-form text.
128
+ */
129
+ export const InvalidWebhookErrorMessages = {
130
+ signatureMismatch: 'signature mismatch',
131
+ invalidBase64: 'invalid base64 encoding',
132
+ gzipFailed: 'gzip decompression failed',
133
+ invalidJson: 'invalid JSON payload',
134
+ } as const;
135
+
136
+ /**
137
+ * Thrown by {@link verifyAndParseWebhook} when the supplied `x-signature` does not
138
+ * match the HMAC of the uncompressed payload, and by all webhook helpers (including
139
+ * {@link parseSqs} / {@link parseSns}) when a gzip / base64 / JSON envelope is malformed.
140
+ *
141
+ * The message identifies which failure mode fired. See
142
+ * {@link InvalidWebhookErrorMessages} for the canonical strings.
143
+ */
144
+ export class InvalidWebhookError extends Error {
145
+ public name = 'InvalidWebhookError';
146
+
147
+ constructor(message: string = InvalidWebhookErrorMessages.signatureMismatch) {
148
+ super(message);
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Returns `body` as a `Buffer`, gzip-decompressed when its first two
154
+ * bytes match the gzip magic (`1f 8b`, per RFC 1952). When the body is
155
+ * plain JSON (no compression, or middleware already decompressed), the
156
+ * bytes are returned unchanged.
157
+ *
158
+ * Magic-byte detection (rather than relying on a header) keeps the
159
+ * same handler correct when middleware - Express, Next.js, AWS Lambda
160
+ * - auto-decompresses the request before your code sees it.
161
+ */
162
+ export function gunzipPayload(rawBody: string | Buffer): Buffer {
163
+ const GZIP_MAGIC = Buffer.from([0x1f, 0x8b]);
164
+
165
+ const body = Buffer.isBuffer(rawBody) ? rawBody : Buffer.from(rawBody);
166
+ if (body.length >= 2 && body.subarray(0, 2).equals(GZIP_MAGIC)) {
167
+ try {
168
+ return zlib.gunzipSync(body);
169
+ } catch {
170
+ throw new InvalidWebhookError(InvalidWebhookErrorMessages.gzipFailed);
171
+ }
172
+ }
173
+ return body;
174
+ }
175
+
176
+ /**
177
+ * Reverses the SQS firehose envelope: the message `Body` is
178
+ * base64-decoded, then the result is gzip-decompressed when it begins
179
+ * with the gzip magic. Returns the raw JSON `Buffer` Stream signed.
180
+ *
181
+ * SQS bodies are always base64-encoded so they remain valid UTF-8 over
182
+ * the queue. The same call works whether or not Stream is currently
183
+ * compressing payloads for this app.
184
+ */
185
+ export function decodeSqsPayload(body: string): Buffer {
186
+ // Reject anything that isn't canonical base64 up front. Node's base64
187
+ // decoder is permissive (silently strips characters outside the
188
+ // alphabet, accepts both standard and URL-safe variants), so we have
189
+ // to be strict here to avoid silently corrupting the body before the
190
+ // signature check runs.
191
+ if (!/^[A-Za-z0-9+/]*={0,2}$/.test(body) || body.length % 4 !== 0) {
192
+ throw new InvalidWebhookError(InvalidWebhookErrorMessages.invalidBase64);
193
+ }
194
+ const decoded = Buffer.from(body, 'base64');
195
+ if (decoded.toString('base64').length !== body.length) {
196
+ throw new InvalidWebhookError(InvalidWebhookErrorMessages.invalidBase64);
197
+ }
198
+ return gunzipPayload(decoded);
199
+ }
200
+
201
+ /**
202
+ * Reverses an SNS HTTP notification envelope. When `notificationBody`
203
+ * is a JSON envelope (`{"Type":"Notification","Message":"..."}`), the
204
+ * inner `Message` field is extracted and run through the SQS pipeline
205
+ * (base64-decode, then gzip-if-magic). When the input is not a JSON
206
+ * envelope it is treated as the already-extracted `Message` string,
207
+ * so call sites that pre-unwrap continue to work.
208
+ */
209
+ export function decodeSnsPayload(notificationBody: string): Buffer {
210
+ const inner = extractSnsMessage(notificationBody);
211
+ return decodeSqsPayload(inner ?? notificationBody);
212
+ }
213
+
214
+ function extractSnsMessage(notificationBody: string): string | null {
215
+ const trimmed = notificationBody.replace(/^[\s\uFEFF]+/, '');
216
+ if (!trimmed.startsWith('{')) {
217
+ return null;
218
+ }
219
+ let parsed: unknown;
220
+ try {
221
+ parsed = JSON.parse(trimmed);
222
+ } catch {
223
+ return null;
224
+ }
225
+ if (
226
+ parsed === null ||
227
+ typeof parsed !== 'object' ||
228
+ Array.isArray(parsed) ||
229
+ typeof (parsed as { Message?: unknown }).Message !== 'string'
230
+ ) {
231
+ return null;
232
+ }
233
+ return (parsed as { Message: string }).Message;
234
+ }
235
+
236
+ /**
237
+ * Parse a JSON-encoded webhook event into a typed {@link Event}. New
238
+ * event types Stream introduces still parse successfully - the runtime
239
+ * shape is the JSON Stream sent and the `type` field stays preserved.
240
+ */
241
+ export function parseEvent(payload: Buffer | string): Event {
242
+ const text = Buffer.isBuffer(payload) ? payload.toString('utf8') : payload;
243
+ try {
244
+ return JSON.parse(text) as Event;
245
+ } catch {
246
+ throw new InvalidWebhookError(InvalidWebhookErrorMessages.invalidJson);
247
+ }
248
+ }
249
+
250
+ function verifyAndParse(payload: Buffer, signature: string, secret: string): Event {
251
+ if (!verifySignature(payload, signature, secret)) {
252
+ throw new InvalidWebhookError(InvalidWebhookErrorMessages.signatureMismatch);
253
+ }
254
+ return parseEvent(payload);
255
+ }
256
+
257
+ /**
258
+ * Decompress (when gzipped), verify the HMAC `signature`, and return
259
+ * the parsed {@link Event}.
260
+ *
261
+ * @param rawBody Raw HTTP request body bytes Stream signed
262
+ * @param signature Value of the `X-Signature` header
263
+ * @param secret Your app's API secret
264
+ * @throws {InvalidWebhookError} When the signature does not match or
265
+ * the gzip envelope is malformed.
266
+ */
267
+ export function verifyAndParseWebhook(
268
+ rawBody: string | Buffer,
269
+ signature: string,
270
+ secret: string,
271
+ ): Event {
272
+ return verifyAndParse(gunzipPayload(rawBody), signature, secret);
273
+ }
274
+
275
+ /**
276
+ * Decode the SQS message `Body` (base64, then gzip-if-magic) and return
277
+ * the parsed {@link Event}. Stream does not attach an application-level HMAC
278
+ * to SQS deliveries — use {@link verifyAndParseWebhook} for HTTP webhooks.
279
+ */
280
+ export function parseSqs(messageBody: string): Event {
281
+ return parseEvent(decodeSqsPayload(messageBody));
282
+ }
283
+
284
+ /**
285
+ * Decode an SNS notification (unwrap the JSON envelope when needed; same
286
+ * inner format as SQS). No application-level HMAC verification.
287
+ */
288
+ export function parseSns(notificationBody: string): Event {
289
+ return parseEvent(decodeSnsPayload(notificationBody));
290
+ }
package/src/utils.ts CHANGED
@@ -479,7 +479,7 @@ export const deleteUserMessages = ({
479
479
  : (toDeletedMessage({ message, hardDelete, deletedAt }) as LocalMessage);
480
480
  }
481
481
 
482
- if (message.quoted_message?.user?.id === user.id) {
482
+ if (messages[i].quoted_message && message.quoted_message?.user?.id === user.id) {
483
483
  messages[i].quoted_message =
484
484
  message.quoted_message.type === 'deleted'
485
485
  ? message.quoted_message