stream-chat 9.43.2 → 9.44.0
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.
- package/README.md +1 -1
- package/dist/cjs/index.browser.js +148 -6
- package/dist/cjs/index.browser.js.map +3 -3
- package/dist/cjs/index.node.js +153 -7
- package/dist/cjs/index.node.js.map +3 -3
- package/dist/esm/index.mjs +148 -6
- package/dist/esm/index.mjs.map +3 -3
- package/dist/types/client.d.ts +32 -0
- package/dist/types/signing.d.ts +99 -5
- package/package.json +2 -1
- package/src/channel.ts +3 -1
- package/src/client.ts +56 -2
- package/src/messageComposer/messageComposer.ts +2 -0
- package/src/signing.ts +195 -7
- package/src/utils.ts +1 -1
package/dist/types/client.d.ts
CHANGED
|
@@ -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
|
package/dist/types/signing.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
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.
|
|
3
|
+
"version": "9.44.0",
|
|
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
|
-
|
|
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
|
}
|
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 {
|
|
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 &&
|
|
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
|
|
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
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
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
|
|
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
|