@stream-io/video-client 0.0.1-alpha.7
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/LICENSE +219 -0
- package/README.md +14 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +14663 -0
- package/dist/index.js.map +1 -0
- package/dist/src/Batcher.d.ts +12 -0
- package/dist/src/CallDropScheduler.d.ts +44 -0
- package/dist/src/StreamSfuClient.d.ts +25 -0
- package/dist/src/StreamVideoClient.d.ts +145 -0
- package/dist/src/__tests__/StreamVideoClient.test.d.ts +1 -0
- package/dist/src/config/defaultConfigs.d.ts +2 -0
- package/dist/src/config/types.d.ts +29 -0
- package/dist/src/coordinator/StreamCoordinatorClient.d.ts +19 -0
- package/dist/src/coordinator/connection/base64.d.ts +2 -0
- package/dist/src/coordinator/connection/client.d.ts +174 -0
- package/dist/src/coordinator/connection/connection.d.ts +139 -0
- package/dist/src/coordinator/connection/connection_fallback.d.ts +38 -0
- package/dist/src/coordinator/connection/errors.d.ts +16 -0
- package/dist/src/coordinator/connection/events.d.ts +7 -0
- package/dist/src/coordinator/connection/insights.d.ts +58 -0
- package/dist/src/coordinator/connection/signing.d.ts +30 -0
- package/dist/src/coordinator/connection/token_manager.d.ts +39 -0
- package/dist/src/coordinator/connection/types.d.ts +96 -0
- package/dist/src/coordinator/connection/utils.d.ts +25 -0
- package/dist/src/devices.d.ts +79 -0
- package/dist/src/events/call.d.ts +26 -0
- package/dist/src/events/internal.d.ts +8 -0
- package/dist/src/events/participant.d.ts +21 -0
- package/dist/src/events/speaker.d.ts +10 -0
- package/dist/src/gen/coordinator/index.d.ts +1664 -0
- package/dist/src/gen/google/protobuf/descriptor.d.ts +1650 -0
- package/dist/src/gen/google/protobuf/duration.d.ts +113 -0
- package/dist/src/gen/google/protobuf/struct.d.ts +184 -0
- package/dist/src/gen/google/protobuf/timestamp.d.ts +158 -0
- package/dist/src/gen/video/coordinator/broadcast_v1/broadcast.d.ts +66 -0
- package/dist/src/gen/video/coordinator/call_v1/call.d.ts +254 -0
- package/dist/src/gen/video/coordinator/client_v1_rpc/client_rpc.client.d.ts +351 -0
- package/dist/src/gen/video/coordinator/client_v1_rpc/client_rpc.d.ts +1488 -0
- package/dist/src/gen/video/coordinator/client_v1_rpc/envelopes.d.ts +143 -0
- package/dist/src/gen/video/coordinator/client_v1_rpc/websocket.d.ts +292 -0
- package/dist/src/gen/video/coordinator/edge_v1/edge.d.ts +183 -0
- package/dist/src/gen/video/coordinator/event_v1/event.d.ts +411 -0
- package/dist/src/gen/video/coordinator/geofence_v1/geofence.d.ts +63 -0
- package/dist/src/gen/video/coordinator/member_v1/member.d.ts +59 -0
- package/dist/src/gen/video/coordinator/participant_v1/participant.d.ts +103 -0
- package/dist/src/gen/video/coordinator/push_v1/push.d.ts +240 -0
- package/dist/src/gen/video/coordinator/stat_v1/stat.d.ts +308 -0
- package/dist/src/gen/video/coordinator/user_v1/user.d.ts +112 -0
- package/dist/src/gen/video/coordinator/utils_v1/utils.d.ts +47 -0
- package/dist/src/gen/video/sfu/event/events.d.ts +736 -0
- package/dist/src/gen/video/sfu/models/models.d.ts +460 -0
- package/dist/src/gen/video/sfu/signal_rpc/signal.client.d.ts +89 -0
- package/dist/src/gen/video/sfu/signal_rpc/signal.d.ts +320 -0
- package/dist/src/helpers/browsers.d.ts +8 -0
- package/dist/src/helpers/sound-detector.d.ts +34 -0
- package/dist/src/rpc/createClient.d.ts +10 -0
- package/dist/src/rpc/index.d.ts +2 -0
- package/dist/src/rpc/latency.d.ts +9 -0
- package/dist/src/rtc/Call.d.ts +180 -0
- package/dist/src/rtc/CallMetadata.d.ts +9 -0
- package/dist/src/rtc/Dispatcher.d.ts +9 -0
- package/dist/src/rtc/IceTrickleBuffer.d.ts +11 -0
- package/dist/src/rtc/callEventHandlers.d.ts +5 -0
- package/dist/src/rtc/codecs.d.ts +2 -0
- package/dist/src/rtc/helpers/iceCandidate.d.ts +2 -0
- package/dist/src/rtc/helpers/tracks.d.ts +3 -0
- package/dist/src/rtc/publisher.d.ts +53 -0
- package/dist/src/rtc/signal.d.ts +5 -0
- package/dist/src/rtc/subscriber.d.ts +7 -0
- package/dist/src/rtc/types.d.ts +84 -0
- package/dist/src/rtc/videoLayers.d.ts +17 -0
- package/dist/src/stats/coordinator-stats-reporter.d.ts +10 -0
- package/dist/src/stats/state-store-stats-reporter.d.ts +57 -0
- package/dist/src/stats/types.d.ts +42 -0
- package/dist/src/store/index.d.ts +2 -0
- package/dist/src/store/rxUtils.d.ts +18 -0
- package/dist/src/store/stateStore.d.ts +182 -0
- package/generate-openapi.sh +32 -0
- package/index.ts +30 -0
- package/openapitools.json +7 -0
- package/package.json +54 -0
- package/rollup.config.mjs +48 -0
- package/src/Batcher.ts +43 -0
- package/src/CallDropScheduler.ts +192 -0
- package/src/StreamSfuClient.ts +185 -0
- package/src/StreamVideoClient.ts +487 -0
- package/src/__tests__/StreamVideoClient.test.ts +83 -0
- package/src/config/defaultConfigs.ts +15 -0
- package/src/config/types.ts +30 -0
- package/src/coordinator/StreamCoordinatorClient.ts +111 -0
- package/src/coordinator/connection/base64.ts +80 -0
- package/src/coordinator/connection/client.ts +815 -0
- package/src/coordinator/connection/connection.ts +750 -0
- package/src/coordinator/connection/connection_fallback.ts +239 -0
- package/src/coordinator/connection/errors.ts +70 -0
- package/src/coordinator/connection/events.ts +10 -0
- package/src/coordinator/connection/insights.ts +88 -0
- package/src/coordinator/connection/signing.ts +104 -0
- package/src/coordinator/connection/token_manager.ts +160 -0
- package/src/coordinator/connection/types.ts +120 -0
- package/src/coordinator/connection/utils.ts +148 -0
- package/src/devices.ts +266 -0
- package/src/events/call.ts +166 -0
- package/src/events/internal.ts +47 -0
- package/src/events/participant.ts +97 -0
- package/src/events/speaker.ts +62 -0
- package/src/gen/coordinator/index.ts +1653 -0
- package/src/gen/google/protobuf/descriptor.ts +3466 -0
- package/src/gen/google/protobuf/duration.ts +232 -0
- package/src/gen/google/protobuf/struct.ts +481 -0
- package/src/gen/google/protobuf/timestamp.ts +291 -0
- package/src/gen/video/coordinator/broadcast_v1/broadcast.ts +154 -0
- package/src/gen/video/coordinator/call_v1/call.ts +651 -0
- package/src/gen/video/coordinator/client_v1_rpc/client_rpc.client.ts +463 -0
- package/src/gen/video/coordinator/client_v1_rpc/client_rpc.ts +3819 -0
- package/src/gen/video/coordinator/client_v1_rpc/envelopes.ts +424 -0
- package/src/gen/video/coordinator/client_v1_rpc/websocket.ts +719 -0
- package/src/gen/video/coordinator/edge_v1/edge.ts +532 -0
- package/src/gen/video/coordinator/event_v1/event.ts +1171 -0
- package/src/gen/video/coordinator/geofence_v1/geofence.ts +128 -0
- package/src/gen/video/coordinator/member_v1/member.ts +138 -0
- package/src/gen/video/coordinator/participant_v1/participant.ts +261 -0
- package/src/gen/video/coordinator/push_v1/push.ts +651 -0
- package/src/gen/video/coordinator/stat_v1/stat.ts +656 -0
- package/src/gen/video/coordinator/user_v1/user.ts +277 -0
- package/src/gen/video/coordinator/utils_v1/utils.ts +98 -0
- package/src/gen/video/sfu/event/events.ts +1962 -0
- package/src/gen/video/sfu/models/models.ts +1062 -0
- package/src/gen/video/sfu/signal_rpc/signal.client.ts +108 -0
- package/src/gen/video/sfu/signal_rpc/signal.ts +906 -0
- package/src/helpers/browsers.ts +13 -0
- package/src/helpers/sound-detector.ts +85 -0
- package/src/rpc/createClient.ts +50 -0
- package/src/rpc/index.ts +2 -0
- package/src/rpc/latency.ts +43 -0
- package/src/rtc/Call.ts +585 -0
- package/src/rtc/CallMetadata.ts +24 -0
- package/src/rtc/Dispatcher.ts +46 -0
- package/src/rtc/IceTrickleBuffer.ts +21 -0
- package/src/rtc/callEventHandlers.ts +37 -0
- package/src/rtc/codecs.ts +61 -0
- package/src/rtc/helpers/iceCandidate.ts +16 -0
- package/src/rtc/helpers/tracks.ts +18 -0
- package/src/rtc/publisher.ts +305 -0
- package/src/rtc/signal.ts +34 -0
- package/src/rtc/subscriber.ts +85 -0
- package/src/rtc/types.ts +105 -0
- package/src/rtc/videoLayers.ts +103 -0
- package/src/stats/coordinator-stats-reporter.ts +167 -0
- package/src/stats/state-store-stats-reporter.ts +364 -0
- package/src/stats/types.ts +46 -0
- package/src/store/index.ts +2 -0
- package/src/store/rxUtils.ts +42 -0
- package/src/store/stateStore.ts +341 -0
- package/tsconfig.json +25 -0
- package/typedoc.json +11 -0
- package/vite.config.ts +11 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import axios, { AxiosRequestConfig, CancelTokenSource } from 'axios';
|
|
2
|
+
import { StreamClient } from './client';
|
|
3
|
+
import {
|
|
4
|
+
addConnectionEventListeners,
|
|
5
|
+
removeConnectionEventListeners,
|
|
6
|
+
retryInterval,
|
|
7
|
+
sleep,
|
|
8
|
+
} from './utils';
|
|
9
|
+
import { isAPIError, isConnectionIDError, isErrorRetryable } from './errors';
|
|
10
|
+
import { ConnectionOpen, Event, UR, LogLevel } from './types';
|
|
11
|
+
|
|
12
|
+
export enum ConnectionState {
|
|
13
|
+
Closed = 'CLOSED',
|
|
14
|
+
Connected = 'CONNECTED',
|
|
15
|
+
Connecting = 'CONNECTING',
|
|
16
|
+
Disconnected = 'DISCONNECTED',
|
|
17
|
+
Init = 'INIT',
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class WSConnectionFallback {
|
|
21
|
+
client: StreamClient;
|
|
22
|
+
state: ConnectionState;
|
|
23
|
+
consecutiveFailures: number;
|
|
24
|
+
connectionID?: string;
|
|
25
|
+
cancelToken?: CancelTokenSource;
|
|
26
|
+
|
|
27
|
+
constructor(client: StreamClient) {
|
|
28
|
+
this.client = client;
|
|
29
|
+
this.state = ConnectionState.Init;
|
|
30
|
+
this.consecutiveFailures = 0;
|
|
31
|
+
|
|
32
|
+
addConnectionEventListeners(this._onlineStatusChanged);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
_log(msg: string, extra: UR = {}, level: LogLevel = 'info') {
|
|
36
|
+
this.client.logger(level, 'WSConnectionFallback:' + msg, {
|
|
37
|
+
tags: ['connection_fallback', 'connection'],
|
|
38
|
+
...extra,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
_setState(state: ConnectionState) {
|
|
43
|
+
this._log(`_setState() - ${state}`);
|
|
44
|
+
|
|
45
|
+
// transition from connecting => connected
|
|
46
|
+
if (
|
|
47
|
+
this.state === ConnectionState.Connecting &&
|
|
48
|
+
state === ConnectionState.Connected
|
|
49
|
+
) {
|
|
50
|
+
this.client.dispatchEvent({ type: 'connection.changed', online: true });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (
|
|
54
|
+
state === ConnectionState.Closed ||
|
|
55
|
+
state === ConnectionState.Disconnected
|
|
56
|
+
) {
|
|
57
|
+
this.client.dispatchEvent({ type: 'connection.changed', online: false });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
this.state = state;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** @private */
|
|
64
|
+
_onlineStatusChanged = (event: { type: string }) => {
|
|
65
|
+
this._log(`_onlineStatusChanged() - ${event.type}`);
|
|
66
|
+
|
|
67
|
+
if (event.type === 'offline') {
|
|
68
|
+
this._setState(ConnectionState.Closed);
|
|
69
|
+
this.cancelToken?.cancel('disconnect() is called');
|
|
70
|
+
this.cancelToken = undefined;
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (event.type === 'online' && this.state === ConnectionState.Closed) {
|
|
75
|
+
this.connect(true);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/** @private */
|
|
80
|
+
_req = async <T = UR>(
|
|
81
|
+
params: UR,
|
|
82
|
+
config: AxiosRequestConfig,
|
|
83
|
+
retry: boolean,
|
|
84
|
+
): Promise<T> => {
|
|
85
|
+
if (!this.cancelToken && !params.close) {
|
|
86
|
+
this.cancelToken = axios.CancelToken.source();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const res = await this.client.doAxiosRequest<T>(
|
|
91
|
+
'get',
|
|
92
|
+
(this.client.baseURL as string).replace(':3030', ':8900') + '/longpoll', // replace port if present for testing with local API
|
|
93
|
+
undefined,
|
|
94
|
+
{
|
|
95
|
+
config: { ...config, cancelToken: this.cancelToken?.token },
|
|
96
|
+
params,
|
|
97
|
+
},
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
this.consecutiveFailures = 0; // always reset in case of no error
|
|
101
|
+
return res;
|
|
102
|
+
} catch (err) {
|
|
103
|
+
this.consecutiveFailures += 1;
|
|
104
|
+
|
|
105
|
+
// @ts-ignore
|
|
106
|
+
if (retry && isErrorRetryable(err)) {
|
|
107
|
+
this._log(`_req() - Retryable error, retrying request`);
|
|
108
|
+
await sleep(retryInterval(this.consecutiveFailures));
|
|
109
|
+
return this._req<T>(params, config, retry);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
throw err;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/** @private */
|
|
117
|
+
_poll = async () => {
|
|
118
|
+
while (this.state === ConnectionState.Connected) {
|
|
119
|
+
try {
|
|
120
|
+
const data = await this._req<{
|
|
121
|
+
events: Event[];
|
|
122
|
+
}>(
|
|
123
|
+
{},
|
|
124
|
+
{
|
|
125
|
+
timeout: 30000,
|
|
126
|
+
},
|
|
127
|
+
true,
|
|
128
|
+
); // 30s => API responds in 20s if there is no event
|
|
129
|
+
|
|
130
|
+
if (data.events?.length) {
|
|
131
|
+
for (let i = 0; i < data.events.length; i++) {
|
|
132
|
+
this.client.dispatchEvent(data.events[i]);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
} catch (err) {
|
|
136
|
+
if (axios.isCancel(err)) {
|
|
137
|
+
this._log(`_poll() - axios canceled request`);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** client.doAxiosRequest will take care of TOKEN_EXPIRED error */
|
|
142
|
+
|
|
143
|
+
// @ts-ignore
|
|
144
|
+
if (isConnectionIDError(err)) {
|
|
145
|
+
this._log(`_poll() - ConnectionID error, connecting without ID...`);
|
|
146
|
+
this._setState(ConnectionState.Disconnected);
|
|
147
|
+
this.connect(true);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// @ts-ignore
|
|
152
|
+
if (isAPIError(err) && !isErrorRetryable(err)) {
|
|
153
|
+
this._setState(ConnectionState.Closed);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
await sleep(retryInterval(this.consecutiveFailures));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* connect try to open a longpoll request
|
|
164
|
+
* @param reconnect should be false for first call and true for subsequent calls to keep the connection alive and call recoverState
|
|
165
|
+
*/
|
|
166
|
+
connect = async (reconnect = false) => {
|
|
167
|
+
if (this.state === ConnectionState.Connecting) {
|
|
168
|
+
this._log(
|
|
169
|
+
'connect() - connecting already in progress',
|
|
170
|
+
{ reconnect },
|
|
171
|
+
'warn',
|
|
172
|
+
);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (this.state === ConnectionState.Connected) {
|
|
176
|
+
this._log(
|
|
177
|
+
'connect() - already connected and polling',
|
|
178
|
+
{ reconnect },
|
|
179
|
+
'warn',
|
|
180
|
+
);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
this._setState(ConnectionState.Connecting);
|
|
185
|
+
this.connectionID = undefined; // connect should be sent with empty connection_id so API creates one
|
|
186
|
+
try {
|
|
187
|
+
const { event } = await this._req<{
|
|
188
|
+
event: ConnectionOpen;
|
|
189
|
+
}>(
|
|
190
|
+
{ json: this.client._buildWSPayload() },
|
|
191
|
+
{
|
|
192
|
+
timeout: 8000, // 8s
|
|
193
|
+
},
|
|
194
|
+
reconnect,
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
this._setState(ConnectionState.Connected);
|
|
198
|
+
this.connectionID = event.connection_id;
|
|
199
|
+
// @ts-expect-error
|
|
200
|
+
this.client.dispatchEvent(event);
|
|
201
|
+
this._poll();
|
|
202
|
+
return event;
|
|
203
|
+
} catch (err) {
|
|
204
|
+
this._setState(ConnectionState.Closed);
|
|
205
|
+
throw err;
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* isHealthy checks if there is a connectionID and connection is in Connected state
|
|
211
|
+
*/
|
|
212
|
+
isHealthy = () => {
|
|
213
|
+
return !!this.connectionID && this.state === ConnectionState.Connected;
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
disconnect = async (timeout = 2000) => {
|
|
217
|
+
removeConnectionEventListeners(this._onlineStatusChanged);
|
|
218
|
+
|
|
219
|
+
this._setState(ConnectionState.Disconnected);
|
|
220
|
+
this.cancelToken?.cancel('disconnect() is called');
|
|
221
|
+
this.cancelToken = undefined;
|
|
222
|
+
|
|
223
|
+
const connection_id = this.connectionID;
|
|
224
|
+
this.connectionID = undefined;
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
await this._req(
|
|
228
|
+
{ close: true, connection_id },
|
|
229
|
+
{
|
|
230
|
+
timeout,
|
|
231
|
+
},
|
|
232
|
+
false,
|
|
233
|
+
);
|
|
234
|
+
this._log(`disconnect() - Closed connectionID`);
|
|
235
|
+
} catch (err) {
|
|
236
|
+
this._log(`disconnect() - Failed`, { err }, 'error');
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { AxiosResponse } from 'axios';
|
|
2
|
+
import { APIErrorResponse } from './types';
|
|
3
|
+
|
|
4
|
+
export const APIErrorCodes: Record<
|
|
5
|
+
string,
|
|
6
|
+
{ name: string; retryable: boolean }
|
|
7
|
+
> = {
|
|
8
|
+
'-1': { name: 'InternalSystemError', retryable: true },
|
|
9
|
+
'2': { name: 'AccessKeyError', retryable: false },
|
|
10
|
+
'3': { name: 'AuthenticationFailedError', retryable: true },
|
|
11
|
+
'4': { name: 'InputError', retryable: false },
|
|
12
|
+
'6': { name: 'DuplicateUsernameError', retryable: false },
|
|
13
|
+
'9': { name: 'RateLimitError', retryable: true },
|
|
14
|
+
'16': { name: 'DoesNotExistError', retryable: false },
|
|
15
|
+
'17': { name: 'NotAllowedError', retryable: false },
|
|
16
|
+
'18': { name: 'EventNotSupportedError', retryable: false },
|
|
17
|
+
'19': { name: 'ChannelFeatureNotSupportedError', retryable: false },
|
|
18
|
+
'20': { name: 'MessageTooLongError', retryable: false },
|
|
19
|
+
'21': { name: 'MultipleNestingLevelError', retryable: false },
|
|
20
|
+
'22': { name: 'PayloadTooBigError', retryable: false },
|
|
21
|
+
'23': { name: 'RequestTimeoutError', retryable: true },
|
|
22
|
+
'24': { name: 'MaxHeaderSizeExceededError', retryable: false },
|
|
23
|
+
'40': { name: 'AuthErrorTokenExpired', retryable: false },
|
|
24
|
+
'41': { name: 'AuthErrorTokenNotValidYet', retryable: false },
|
|
25
|
+
'42': { name: 'AuthErrorTokenUsedBeforeIssuedAt', retryable: false },
|
|
26
|
+
'43': { name: 'AuthErrorTokenSignatureInvalid', retryable: false },
|
|
27
|
+
'44': { name: 'CustomCommandEndpointMissingError', retryable: false },
|
|
28
|
+
'45': { name: 'CustomCommandEndpointCallError', retryable: true },
|
|
29
|
+
'46': { name: 'ConnectionIDNotFoundError', retryable: false },
|
|
30
|
+
'60': { name: 'CoolDownError', retryable: true },
|
|
31
|
+
'69': { name: 'ErrWrongRegion', retryable: false },
|
|
32
|
+
'70': { name: 'ErrQueryChannelPermissions', retryable: false },
|
|
33
|
+
'71': { name: 'ErrTooManyConnections', retryable: true },
|
|
34
|
+
'99': { name: 'AppSuspendedError', retryable: false },
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
type APIError = Error & { code: number; isWSFailure?: boolean };
|
|
38
|
+
|
|
39
|
+
export function isAPIError(error: Error): error is APIError {
|
|
40
|
+
return (error as APIError).code !== undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function isErrorRetryable(error: APIError) {
|
|
44
|
+
if (!error.code) return false;
|
|
45
|
+
const err = APIErrorCodes[`${error.code}`];
|
|
46
|
+
if (!err) return false;
|
|
47
|
+
return err.retryable;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function isConnectionIDError(error: APIError) {
|
|
51
|
+
return error.code === 46; // ConnectionIDNotFoundError
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function isWSFailure(err: APIError): boolean {
|
|
55
|
+
if (typeof err.isWSFailure === 'boolean') {
|
|
56
|
+
return err.isWSFailure;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
return JSON.parse(err.message).isWSFailure;
|
|
61
|
+
} catch (_) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function isErrorResponse(
|
|
67
|
+
res: AxiosResponse<unknown>,
|
|
68
|
+
): res is AxiosResponse<APIErrorResponse> {
|
|
69
|
+
return !res.status || res.status < 200 || 300 <= res.status;
|
|
70
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { StableWSConnection } from './connection';
|
|
3
|
+
import { randomId, sleep } from './utils';
|
|
4
|
+
|
|
5
|
+
export type InsightTypes =
|
|
6
|
+
| 'ws_fatal'
|
|
7
|
+
| 'ws_success_after_failure'
|
|
8
|
+
| 'http_hi_failed';
|
|
9
|
+
export class InsightMetrics {
|
|
10
|
+
connectionStartTimestamp: number | null;
|
|
11
|
+
wsConsecutiveFailures: number;
|
|
12
|
+
wsTotalFailures: number;
|
|
13
|
+
instanceClientId: string;
|
|
14
|
+
|
|
15
|
+
constructor() {
|
|
16
|
+
this.connectionStartTimestamp = null;
|
|
17
|
+
this.wsTotalFailures = 0;
|
|
18
|
+
this.wsConsecutiveFailures = 0;
|
|
19
|
+
this.instanceClientId = randomId();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* postInsights is not supposed to be used by end users directly within chat application, and thus is kept isolated
|
|
25
|
+
* from all the client/connection code/logic.
|
|
26
|
+
*
|
|
27
|
+
* @param insightType
|
|
28
|
+
* @param insights
|
|
29
|
+
*/
|
|
30
|
+
export const postInsights = async (
|
|
31
|
+
insightType: InsightTypes,
|
|
32
|
+
insights: Record<string, unknown>,
|
|
33
|
+
) => {
|
|
34
|
+
const maxAttempts = 3;
|
|
35
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
36
|
+
try {
|
|
37
|
+
await axios.post(
|
|
38
|
+
`https://chat-insights.getstream.io/insights/${insightType}`,
|
|
39
|
+
insights,
|
|
40
|
+
);
|
|
41
|
+
} catch (e) {
|
|
42
|
+
await sleep((i + 1) * 3000);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export function buildWsFatalInsight(
|
|
50
|
+
connection: StableWSConnection,
|
|
51
|
+
event: Record<string, unknown>,
|
|
52
|
+
) {
|
|
53
|
+
return {
|
|
54
|
+
...event,
|
|
55
|
+
...buildWsBaseInsight(connection),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function buildWsBaseInsight(connection: StableWSConnection) {
|
|
60
|
+
const { client } = connection;
|
|
61
|
+
return {
|
|
62
|
+
ready_state: connection.ws?.readyState,
|
|
63
|
+
url: connection._buildUrl(),
|
|
64
|
+
api_key: client.key,
|
|
65
|
+
start_ts: client.insightMetrics.connectionStartTimestamp,
|
|
66
|
+
end_ts: new Date().getTime(),
|
|
67
|
+
auth_type: client.getAuthType(),
|
|
68
|
+
token: client.tokenManager.token,
|
|
69
|
+
user_id: client.userID,
|
|
70
|
+
user_details: client._user,
|
|
71
|
+
// device: client.options.device,
|
|
72
|
+
device: 'browser',
|
|
73
|
+
client_id: connection.connectionID,
|
|
74
|
+
ws_details: connection.ws,
|
|
75
|
+
ws_consecutive_failures: client.insightMetrics.wsConsecutiveFailures,
|
|
76
|
+
ws_total_failures: client.insightMetrics.wsTotalFailures,
|
|
77
|
+
request_id: connection.requestID,
|
|
78
|
+
online: typeof navigator !== 'undefined' ? navigator?.onLine : null,
|
|
79
|
+
user_agent: typeof navigator !== 'undefined' ? navigator?.userAgent : null,
|
|
80
|
+
instance_client_id: client.insightMetrics.instanceClientId,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function buildWsSuccessAfterFailureInsight(
|
|
85
|
+
connection: StableWSConnection,
|
|
86
|
+
) {
|
|
87
|
+
return buildWsBaseInsight(connection);
|
|
88
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import jwt, { Secret, SignOptions } from 'jsonwebtoken';
|
|
2
|
+
import crypto from 'crypto';
|
|
3
|
+
import { encodeBase64, decodeBase64 } from './base64';
|
|
4
|
+
import { UR } from './types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Creates the JWT token that can be used for a UserSession
|
|
8
|
+
* @method JWTUserToken
|
|
9
|
+
* @memberof signing
|
|
10
|
+
* @private
|
|
11
|
+
* @param {Secret} apiSecret - API Secret key
|
|
12
|
+
* @param {string} userId - The user_id key in the JWT payload
|
|
13
|
+
* @param {UR} [extraData] - Extra that should be part of the JWT token
|
|
14
|
+
* @param {SignOptions} [jwtOptions] - Options that can be past to jwt.sign
|
|
15
|
+
* @return {string} JWT Token
|
|
16
|
+
*/
|
|
17
|
+
export function JWTUserToken(
|
|
18
|
+
apiSecret: Secret,
|
|
19
|
+
userId: string,
|
|
20
|
+
extraData: UR = {},
|
|
21
|
+
jwtOptions: SignOptions = {},
|
|
22
|
+
) {
|
|
23
|
+
if (typeof userId !== 'string') {
|
|
24
|
+
throw new TypeError('userId should be a string');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const payload: { user_id: string } & UR = {
|
|
28
|
+
user_id: userId,
|
|
29
|
+
...extraData,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// make sure we return a clear error when jwt is shimmed (ie. browser build)
|
|
33
|
+
if (jwt == null || jwt.sign == null) {
|
|
34
|
+
throw Error(
|
|
35
|
+
`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.`,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const opts: SignOptions = Object.assign(
|
|
40
|
+
{ algorithm: 'HS256', noTimestamp: true },
|
|
41
|
+
jwtOptions,
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
if (payload.iat) {
|
|
45
|
+
opts.noTimestamp = false;
|
|
46
|
+
}
|
|
47
|
+
return jwt.sign(payload, apiSecret, opts);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function JWTServerToken(
|
|
51
|
+
apiSecret: Secret,
|
|
52
|
+
jwtOptions: SignOptions = {},
|
|
53
|
+
) {
|
|
54
|
+
const payload = {
|
|
55
|
+
server: true,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const opts: SignOptions = Object.assign(
|
|
59
|
+
{ algorithm: 'HS256', noTimestamp: true },
|
|
60
|
+
jwtOptions,
|
|
61
|
+
);
|
|
62
|
+
return jwt.sign(payload, apiSecret, opts);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function UserFromToken(token: string) {
|
|
66
|
+
const fragments = token.split('.');
|
|
67
|
+
if (fragments.length !== 3) {
|
|
68
|
+
return '';
|
|
69
|
+
}
|
|
70
|
+
const b64Payload = fragments[1];
|
|
71
|
+
const payload = decodeBase64(b64Payload);
|
|
72
|
+
const data = JSON.parse(payload);
|
|
73
|
+
return data.user_id as string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
*
|
|
78
|
+
* @param {string} userId the id of the user
|
|
79
|
+
* @return {string}
|
|
80
|
+
*/
|
|
81
|
+
export function DevToken(userId: string) {
|
|
82
|
+
return [
|
|
83
|
+
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9', //{"alg": "HS256", "typ": "JWT"}
|
|
84
|
+
encodeBase64(JSON.stringify({ user_id: userId })),
|
|
85
|
+
'devtoken', // hardcoded signature
|
|
86
|
+
].join('.');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
*
|
|
91
|
+
* @param {string} body the signed message
|
|
92
|
+
* @param {string} secret the shared secret used to generate the signature (Stream API secret)
|
|
93
|
+
* @param {string} signature the signature to validate
|
|
94
|
+
* @return {boolean}
|
|
95
|
+
*/
|
|
96
|
+
export function CheckSignature(
|
|
97
|
+
body: string,
|
|
98
|
+
secret: string,
|
|
99
|
+
signature: string,
|
|
100
|
+
) {
|
|
101
|
+
const key = Buffer.from(secret, 'ascii');
|
|
102
|
+
const hash = crypto.createHmac('sha256', key).update(body).digest('hex');
|
|
103
|
+
return hash === signature;
|
|
104
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { Secret } from 'jsonwebtoken';
|
|
2
|
+
import { UserFromToken, JWTServerToken, JWTUserToken } from './signing';
|
|
3
|
+
import { isFunction } from './utils';
|
|
4
|
+
import { TokenOrProvider, UserResponse } from './types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* TokenManager
|
|
8
|
+
*
|
|
9
|
+
* Handles all the operations around user token.
|
|
10
|
+
*/
|
|
11
|
+
export class TokenManager {
|
|
12
|
+
loadTokenPromise: Promise<string> | null;
|
|
13
|
+
type: 'static' | 'provider';
|
|
14
|
+
secret?: Secret;
|
|
15
|
+
token?: string;
|
|
16
|
+
tokenProvider?: TokenOrProvider;
|
|
17
|
+
user?: UserResponse;
|
|
18
|
+
/**
|
|
19
|
+
* Constructor
|
|
20
|
+
*
|
|
21
|
+
* @param {Secret} secret
|
|
22
|
+
*/
|
|
23
|
+
constructor(secret?: Secret) {
|
|
24
|
+
this.loadTokenPromise = null;
|
|
25
|
+
if (secret) {
|
|
26
|
+
this.secret = secret;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
this.type = 'static';
|
|
30
|
+
|
|
31
|
+
if (this.secret) {
|
|
32
|
+
this.token = JWTServerToken(this.secret);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Set the static string token or token provider.
|
|
38
|
+
* Token provider should return a token string or a promise which resolves to string token.
|
|
39
|
+
*
|
|
40
|
+
* @param {TokenOrProvider} tokenOrProvider
|
|
41
|
+
* @param {UserResponse} user
|
|
42
|
+
*/
|
|
43
|
+
setTokenOrProvider = async (
|
|
44
|
+
tokenOrProvider: TokenOrProvider,
|
|
45
|
+
user: UserResponse,
|
|
46
|
+
) => {
|
|
47
|
+
this.validateToken(tokenOrProvider, user);
|
|
48
|
+
this.user = user;
|
|
49
|
+
|
|
50
|
+
if (isFunction(tokenOrProvider)) {
|
|
51
|
+
this.tokenProvider = tokenOrProvider;
|
|
52
|
+
this.type = 'provider';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (typeof tokenOrProvider === 'string') {
|
|
56
|
+
this.token = tokenOrProvider;
|
|
57
|
+
this.type = 'static';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!tokenOrProvider && this.user && this.secret) {
|
|
61
|
+
this.token = JWTUserToken(this.secret, user.id, {}, {});
|
|
62
|
+
this.type = 'static';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
await this.loadToken();
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Resets the token manager.
|
|
70
|
+
* Useful for client disconnection or switching user.
|
|
71
|
+
*/
|
|
72
|
+
reset = () => {
|
|
73
|
+
this.token = undefined;
|
|
74
|
+
this.user = undefined;
|
|
75
|
+
this.loadTokenPromise = null;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// Validates the user token.
|
|
79
|
+
validateToken = (tokenOrProvider: TokenOrProvider, user: UserResponse) => {
|
|
80
|
+
// allow empty token for anon user
|
|
81
|
+
if (user && user.anon && !tokenOrProvider) return;
|
|
82
|
+
|
|
83
|
+
// Don't allow empty token for non-server side client.
|
|
84
|
+
if (!this.secret && !tokenOrProvider) {
|
|
85
|
+
throw new Error('User token can not be empty');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (
|
|
89
|
+
tokenOrProvider &&
|
|
90
|
+
typeof tokenOrProvider !== 'string' &&
|
|
91
|
+
!isFunction(tokenOrProvider)
|
|
92
|
+
) {
|
|
93
|
+
throw new Error('user token should either be a string or a function');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (typeof tokenOrProvider === 'string') {
|
|
97
|
+
// Allow empty token for anonymous users
|
|
98
|
+
if (user.anon && tokenOrProvider === '') return;
|
|
99
|
+
|
|
100
|
+
const tokenUserId = UserFromToken(tokenOrProvider);
|
|
101
|
+
if (
|
|
102
|
+
tokenOrProvider != null &&
|
|
103
|
+
(tokenUserId == null || tokenUserId === '' || tokenUserId !== user.id)
|
|
104
|
+
) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
'userToken does not have a user_id or is not matching with user.id',
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// Resolves when token is ready. This function is simply to check if loadToken is in progress, in which
|
|
113
|
+
// case a function should wait.
|
|
114
|
+
tokenReady = () => this.loadTokenPromise;
|
|
115
|
+
|
|
116
|
+
// Fetches a token from tokenProvider function and sets in tokenManager.
|
|
117
|
+
// In case of static token, it will simply resolve to static token.
|
|
118
|
+
loadToken = () => {
|
|
119
|
+
// eslint-disable-next-line no-async-promise-executor
|
|
120
|
+
this.loadTokenPromise = new Promise(async (resolve, reject) => {
|
|
121
|
+
if (this.type === 'static') {
|
|
122
|
+
return resolve(this.token as string);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (this.tokenProvider && typeof this.tokenProvider !== 'string') {
|
|
126
|
+
try {
|
|
127
|
+
this.token = await this.tokenProvider();
|
|
128
|
+
} catch (e) {
|
|
129
|
+
return reject(
|
|
130
|
+
new Error(`Call to tokenProvider failed with message: ${e}`),
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
resolve(this.token);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
return this.loadTokenPromise;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// Returns a current token
|
|
141
|
+
getToken = () => {
|
|
142
|
+
if (this.token) {
|
|
143
|
+
return this.token;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (this.user && this.user.anon && !this.token) {
|
|
147
|
+
return this.token;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (this.secret) {
|
|
151
|
+
return JWTServerToken(this.secret);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
throw new Error(
|
|
155
|
+
`Both secret and user tokens are not set. Either client.connectUser wasn't called or client.disconnect was called`,
|
|
156
|
+
);
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
isStatic = () => this.type === 'static';
|
|
160
|
+
}
|