@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.
Files changed (157) hide show
  1. package/LICENSE +219 -0
  2. package/README.md +14 -0
  3. package/dist/index.d.ts +23 -0
  4. package/dist/index.js +14663 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/src/Batcher.d.ts +12 -0
  7. package/dist/src/CallDropScheduler.d.ts +44 -0
  8. package/dist/src/StreamSfuClient.d.ts +25 -0
  9. package/dist/src/StreamVideoClient.d.ts +145 -0
  10. package/dist/src/__tests__/StreamVideoClient.test.d.ts +1 -0
  11. package/dist/src/config/defaultConfigs.d.ts +2 -0
  12. package/dist/src/config/types.d.ts +29 -0
  13. package/dist/src/coordinator/StreamCoordinatorClient.d.ts +19 -0
  14. package/dist/src/coordinator/connection/base64.d.ts +2 -0
  15. package/dist/src/coordinator/connection/client.d.ts +174 -0
  16. package/dist/src/coordinator/connection/connection.d.ts +139 -0
  17. package/dist/src/coordinator/connection/connection_fallback.d.ts +38 -0
  18. package/dist/src/coordinator/connection/errors.d.ts +16 -0
  19. package/dist/src/coordinator/connection/events.d.ts +7 -0
  20. package/dist/src/coordinator/connection/insights.d.ts +58 -0
  21. package/dist/src/coordinator/connection/signing.d.ts +30 -0
  22. package/dist/src/coordinator/connection/token_manager.d.ts +39 -0
  23. package/dist/src/coordinator/connection/types.d.ts +96 -0
  24. package/dist/src/coordinator/connection/utils.d.ts +25 -0
  25. package/dist/src/devices.d.ts +79 -0
  26. package/dist/src/events/call.d.ts +26 -0
  27. package/dist/src/events/internal.d.ts +8 -0
  28. package/dist/src/events/participant.d.ts +21 -0
  29. package/dist/src/events/speaker.d.ts +10 -0
  30. package/dist/src/gen/coordinator/index.d.ts +1664 -0
  31. package/dist/src/gen/google/protobuf/descriptor.d.ts +1650 -0
  32. package/dist/src/gen/google/protobuf/duration.d.ts +113 -0
  33. package/dist/src/gen/google/protobuf/struct.d.ts +184 -0
  34. package/dist/src/gen/google/protobuf/timestamp.d.ts +158 -0
  35. package/dist/src/gen/video/coordinator/broadcast_v1/broadcast.d.ts +66 -0
  36. package/dist/src/gen/video/coordinator/call_v1/call.d.ts +254 -0
  37. package/dist/src/gen/video/coordinator/client_v1_rpc/client_rpc.client.d.ts +351 -0
  38. package/dist/src/gen/video/coordinator/client_v1_rpc/client_rpc.d.ts +1488 -0
  39. package/dist/src/gen/video/coordinator/client_v1_rpc/envelopes.d.ts +143 -0
  40. package/dist/src/gen/video/coordinator/client_v1_rpc/websocket.d.ts +292 -0
  41. package/dist/src/gen/video/coordinator/edge_v1/edge.d.ts +183 -0
  42. package/dist/src/gen/video/coordinator/event_v1/event.d.ts +411 -0
  43. package/dist/src/gen/video/coordinator/geofence_v1/geofence.d.ts +63 -0
  44. package/dist/src/gen/video/coordinator/member_v1/member.d.ts +59 -0
  45. package/dist/src/gen/video/coordinator/participant_v1/participant.d.ts +103 -0
  46. package/dist/src/gen/video/coordinator/push_v1/push.d.ts +240 -0
  47. package/dist/src/gen/video/coordinator/stat_v1/stat.d.ts +308 -0
  48. package/dist/src/gen/video/coordinator/user_v1/user.d.ts +112 -0
  49. package/dist/src/gen/video/coordinator/utils_v1/utils.d.ts +47 -0
  50. package/dist/src/gen/video/sfu/event/events.d.ts +736 -0
  51. package/dist/src/gen/video/sfu/models/models.d.ts +460 -0
  52. package/dist/src/gen/video/sfu/signal_rpc/signal.client.d.ts +89 -0
  53. package/dist/src/gen/video/sfu/signal_rpc/signal.d.ts +320 -0
  54. package/dist/src/helpers/browsers.d.ts +8 -0
  55. package/dist/src/helpers/sound-detector.d.ts +34 -0
  56. package/dist/src/rpc/createClient.d.ts +10 -0
  57. package/dist/src/rpc/index.d.ts +2 -0
  58. package/dist/src/rpc/latency.d.ts +9 -0
  59. package/dist/src/rtc/Call.d.ts +180 -0
  60. package/dist/src/rtc/CallMetadata.d.ts +9 -0
  61. package/dist/src/rtc/Dispatcher.d.ts +9 -0
  62. package/dist/src/rtc/IceTrickleBuffer.d.ts +11 -0
  63. package/dist/src/rtc/callEventHandlers.d.ts +5 -0
  64. package/dist/src/rtc/codecs.d.ts +2 -0
  65. package/dist/src/rtc/helpers/iceCandidate.d.ts +2 -0
  66. package/dist/src/rtc/helpers/tracks.d.ts +3 -0
  67. package/dist/src/rtc/publisher.d.ts +53 -0
  68. package/dist/src/rtc/signal.d.ts +5 -0
  69. package/dist/src/rtc/subscriber.d.ts +7 -0
  70. package/dist/src/rtc/types.d.ts +84 -0
  71. package/dist/src/rtc/videoLayers.d.ts +17 -0
  72. package/dist/src/stats/coordinator-stats-reporter.d.ts +10 -0
  73. package/dist/src/stats/state-store-stats-reporter.d.ts +57 -0
  74. package/dist/src/stats/types.d.ts +42 -0
  75. package/dist/src/store/index.d.ts +2 -0
  76. package/dist/src/store/rxUtils.d.ts +18 -0
  77. package/dist/src/store/stateStore.d.ts +182 -0
  78. package/generate-openapi.sh +32 -0
  79. package/index.ts +30 -0
  80. package/openapitools.json +7 -0
  81. package/package.json +54 -0
  82. package/rollup.config.mjs +48 -0
  83. package/src/Batcher.ts +43 -0
  84. package/src/CallDropScheduler.ts +192 -0
  85. package/src/StreamSfuClient.ts +185 -0
  86. package/src/StreamVideoClient.ts +487 -0
  87. package/src/__tests__/StreamVideoClient.test.ts +83 -0
  88. package/src/config/defaultConfigs.ts +15 -0
  89. package/src/config/types.ts +30 -0
  90. package/src/coordinator/StreamCoordinatorClient.ts +111 -0
  91. package/src/coordinator/connection/base64.ts +80 -0
  92. package/src/coordinator/connection/client.ts +815 -0
  93. package/src/coordinator/connection/connection.ts +750 -0
  94. package/src/coordinator/connection/connection_fallback.ts +239 -0
  95. package/src/coordinator/connection/errors.ts +70 -0
  96. package/src/coordinator/connection/events.ts +10 -0
  97. package/src/coordinator/connection/insights.ts +88 -0
  98. package/src/coordinator/connection/signing.ts +104 -0
  99. package/src/coordinator/connection/token_manager.ts +160 -0
  100. package/src/coordinator/connection/types.ts +120 -0
  101. package/src/coordinator/connection/utils.ts +148 -0
  102. package/src/devices.ts +266 -0
  103. package/src/events/call.ts +166 -0
  104. package/src/events/internal.ts +47 -0
  105. package/src/events/participant.ts +97 -0
  106. package/src/events/speaker.ts +62 -0
  107. package/src/gen/coordinator/index.ts +1653 -0
  108. package/src/gen/google/protobuf/descriptor.ts +3466 -0
  109. package/src/gen/google/protobuf/duration.ts +232 -0
  110. package/src/gen/google/protobuf/struct.ts +481 -0
  111. package/src/gen/google/protobuf/timestamp.ts +291 -0
  112. package/src/gen/video/coordinator/broadcast_v1/broadcast.ts +154 -0
  113. package/src/gen/video/coordinator/call_v1/call.ts +651 -0
  114. package/src/gen/video/coordinator/client_v1_rpc/client_rpc.client.ts +463 -0
  115. package/src/gen/video/coordinator/client_v1_rpc/client_rpc.ts +3819 -0
  116. package/src/gen/video/coordinator/client_v1_rpc/envelopes.ts +424 -0
  117. package/src/gen/video/coordinator/client_v1_rpc/websocket.ts +719 -0
  118. package/src/gen/video/coordinator/edge_v1/edge.ts +532 -0
  119. package/src/gen/video/coordinator/event_v1/event.ts +1171 -0
  120. package/src/gen/video/coordinator/geofence_v1/geofence.ts +128 -0
  121. package/src/gen/video/coordinator/member_v1/member.ts +138 -0
  122. package/src/gen/video/coordinator/participant_v1/participant.ts +261 -0
  123. package/src/gen/video/coordinator/push_v1/push.ts +651 -0
  124. package/src/gen/video/coordinator/stat_v1/stat.ts +656 -0
  125. package/src/gen/video/coordinator/user_v1/user.ts +277 -0
  126. package/src/gen/video/coordinator/utils_v1/utils.ts +98 -0
  127. package/src/gen/video/sfu/event/events.ts +1962 -0
  128. package/src/gen/video/sfu/models/models.ts +1062 -0
  129. package/src/gen/video/sfu/signal_rpc/signal.client.ts +108 -0
  130. package/src/gen/video/sfu/signal_rpc/signal.ts +906 -0
  131. package/src/helpers/browsers.ts +13 -0
  132. package/src/helpers/sound-detector.ts +85 -0
  133. package/src/rpc/createClient.ts +50 -0
  134. package/src/rpc/index.ts +2 -0
  135. package/src/rpc/latency.ts +43 -0
  136. package/src/rtc/Call.ts +585 -0
  137. package/src/rtc/CallMetadata.ts +24 -0
  138. package/src/rtc/Dispatcher.ts +46 -0
  139. package/src/rtc/IceTrickleBuffer.ts +21 -0
  140. package/src/rtc/callEventHandlers.ts +37 -0
  141. package/src/rtc/codecs.ts +61 -0
  142. package/src/rtc/helpers/iceCandidate.ts +16 -0
  143. package/src/rtc/helpers/tracks.ts +18 -0
  144. package/src/rtc/publisher.ts +305 -0
  145. package/src/rtc/signal.ts +34 -0
  146. package/src/rtc/subscriber.ts +85 -0
  147. package/src/rtc/types.ts +105 -0
  148. package/src/rtc/videoLayers.ts +103 -0
  149. package/src/stats/coordinator-stats-reporter.ts +167 -0
  150. package/src/stats/state-store-stats-reporter.ts +364 -0
  151. package/src/stats/types.ts +46 -0
  152. package/src/store/index.ts +2 -0
  153. package/src/store/rxUtils.ts +42 -0
  154. package/src/store/stateStore.ts +341 -0
  155. package/tsconfig.json +25 -0
  156. package/typedoc.json +11 -0
  157. 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,10 @@
1
+ export const EVENT_MAP = {
2
+ 'health.check': true,
3
+
4
+ 'call.created': true,
5
+
6
+ // local events
7
+ 'connection.changed': true,
8
+ 'connection.recovered': true,
9
+ 'transport.changed': true,
10
+ };
@@ -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
+ }