stream-chat 4.4.3-dev.2 → 5.0.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 +4 -13
- package/dist/browser.es.js +1229 -720
- package/dist/browser.es.js.map +1 -1
- package/dist/browser.full-bundle.min.js +1 -1
- package/dist/browser.full-bundle.min.js.map +1 -1
- package/dist/browser.js +1229 -719
- package/dist/browser.js.map +1 -1
- package/dist/index.es.js +1229 -720
- package/dist/index.es.js.map +1 -1
- package/dist/index.js +1229 -719
- package/dist/index.js.map +1 -1
- package/dist/types/base64.d.ts.map +1 -1
- package/dist/types/channel.d.ts +19 -15
- package/dist/types/channel.d.ts.map +1 -1
- package/dist/types/channel_state.d.ts +2 -2
- package/dist/types/channel_state.d.ts.map +1 -1
- package/dist/types/client.d.ts +25 -42
- package/dist/types/client.d.ts.map +1 -1
- package/dist/types/client_state.d.ts +2 -2
- package/dist/types/client_state.d.ts.map +1 -1
- package/dist/types/connection.d.ts +14 -49
- package/dist/types/connection.d.ts.map +1 -1
- package/dist/types/connection_fallback.d.ts +41 -0
- package/dist/types/connection_fallback.d.ts.map +1 -0
- package/dist/types/errors.d.ts +14 -0
- package/dist/types/errors.d.ts.map +1 -0
- package/dist/types/insights.d.ts +17 -10
- package/dist/types/insights.d.ts.map +1 -1
- package/dist/types/permissions.d.ts.map +1 -1
- package/dist/types/signing.d.ts +3 -3
- package/dist/types/signing.d.ts.map +1 -1
- package/dist/types/token_manager.d.ts +2 -2
- package/dist/types/token_manager.d.ts.map +1 -1
- package/dist/types/types.d.ts +94 -88
- package/dist/types/types.d.ts.map +1 -1
- package/dist/types/utils.d.ts +13 -3
- package/dist/types/utils.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/base64.ts +1 -4
- package/src/channel.ts +133 -461
- package/src/channel_state.ts +31 -158
- package/src/client.ts +291 -712
- package/src/client_state.ts +2 -2
- package/src/connection.ts +146 -395
- package/src/connection_fallback.ts +205 -0
- package/src/errors.ts +58 -0
- package/src/insights.ts +38 -32
- package/src/permissions.ts +3 -24
- package/src/signing.ts +6 -17
- package/src/token_manager.ts +6 -18
- package/src/types.ts +268 -512
- package/src/utils.ts +58 -24
- package/CHANGELOG.md +0 -844
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import axios, { AxiosRequestConfig, CancelTokenSource } from 'axios';
|
|
2
|
+
import { StreamChat } from './client';
|
|
3
|
+
import { addConnectionEventListeners, removeConnectionEventListeners, retryInterval, sleep } from './utils';
|
|
4
|
+
import { isAPIError, isConnectionIDError, isErrorRetryable } from './errors';
|
|
5
|
+
import { ConnectionOpen, Event, UnknownType, UR, LiteralStringForUnion, LogLevel } from './types';
|
|
6
|
+
|
|
7
|
+
export enum ConnectionState {
|
|
8
|
+
Closed = 'CLOSED',
|
|
9
|
+
Connected = 'CONNECTED',
|
|
10
|
+
Connecting = 'CONNECTING',
|
|
11
|
+
Disconnected = 'DISCONNECTED',
|
|
12
|
+
Init = 'INIT',
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class WSConnectionFallback<
|
|
16
|
+
AttachmentType extends UR = UR,
|
|
17
|
+
ChannelType extends UR = UR,
|
|
18
|
+
CommandType extends string = LiteralStringForUnion,
|
|
19
|
+
EventType extends UR = UR,
|
|
20
|
+
MessageType extends UR = UR,
|
|
21
|
+
ReactionType extends UR = UR,
|
|
22
|
+
UserType extends UR = UR
|
|
23
|
+
> {
|
|
24
|
+
client: StreamChat<AttachmentType, ChannelType, CommandType, EventType, MessageType, ReactionType, UserType>;
|
|
25
|
+
state: ConnectionState;
|
|
26
|
+
consecutiveFailures: number;
|
|
27
|
+
connectionID?: string;
|
|
28
|
+
cancelToken?: CancelTokenSource;
|
|
29
|
+
|
|
30
|
+
constructor({
|
|
31
|
+
client,
|
|
32
|
+
}: {
|
|
33
|
+
client: StreamChat<AttachmentType, ChannelType, CommandType, EventType, MessageType, ReactionType, UserType>;
|
|
34
|
+
}) {
|
|
35
|
+
this.client = client;
|
|
36
|
+
this.state = ConnectionState.Init;
|
|
37
|
+
this.consecutiveFailures = 0;
|
|
38
|
+
|
|
39
|
+
addConnectionEventListeners(this._onlineStatusChanged);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
_log(msg: string, extra: UR = {}, level: LogLevel = 'info') {
|
|
43
|
+
this.client.logger(level, 'WSConnectionFallback:' + msg, { tags: ['connection_fallback', 'connection'], ...extra });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
_setState(state: ConnectionState) {
|
|
47
|
+
this._log(`_setState() - ${state}`);
|
|
48
|
+
|
|
49
|
+
// transition from connecting => connected
|
|
50
|
+
if (this.state === ConnectionState.Connecting && state === ConnectionState.Connected) {
|
|
51
|
+
//@ts-expect-error
|
|
52
|
+
this.client.dispatchEvent({ type: 'connection.changed', online: true });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (state === ConnectionState.Closed || state === ConnectionState.Disconnected) {
|
|
56
|
+
//@ts-expect-error
|
|
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>(params: UnknownType, config: AxiosRequestConfig, retry: boolean): Promise<T> => {
|
|
81
|
+
if (!this.cancelToken && !params.close) {
|
|
82
|
+
this.cancelToken = axios.CancelToken.source();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const res = await this.client.doAxiosRequest<T>(
|
|
87
|
+
'get',
|
|
88
|
+
(this.client.baseURL as string).replace(':3030', ':8900') + '/longpoll', // replace port if present for testing with local API
|
|
89
|
+
undefined,
|
|
90
|
+
{
|
|
91
|
+
config: { ...config, cancelToken: this.cancelToken?.token },
|
|
92
|
+
params,
|
|
93
|
+
},
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
this.consecutiveFailures = 0; // always reset in case of no error
|
|
97
|
+
return res;
|
|
98
|
+
} catch (err) {
|
|
99
|
+
this.consecutiveFailures += 1;
|
|
100
|
+
|
|
101
|
+
if (retry && isErrorRetryable(err)) {
|
|
102
|
+
this._log(`_req() - Retryable error, retrying request`);
|
|
103
|
+
await sleep(retryInterval(this.consecutiveFailures));
|
|
104
|
+
return this._req<T>(params, config, retry);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
throw err;
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/** @private */
|
|
112
|
+
_poll = async () => {
|
|
113
|
+
while (this.state === ConnectionState.Connected) {
|
|
114
|
+
try {
|
|
115
|
+
const data = await this._req<{
|
|
116
|
+
events: Event<AttachmentType, ChannelType, CommandType, EventType, MessageType, ReactionType, UserType>[];
|
|
117
|
+
}>({}, { timeout: 30000 }, true); // 30s => API responds in 20s if there is no event
|
|
118
|
+
|
|
119
|
+
if (data.events?.length) {
|
|
120
|
+
for (let i = 0; i < data.events.length; i++) {
|
|
121
|
+
this.client.dispatchEvent(data.events[i]);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
} catch (err) {
|
|
125
|
+
if (axios.isCancel(err)) {
|
|
126
|
+
this._log(`_poll() - axios canceled request`);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** client.doAxiosRequest will take care of TOKEN_EXPIRED error */
|
|
131
|
+
|
|
132
|
+
if (isConnectionIDError(err)) {
|
|
133
|
+
this._log(`_poll() - ConnectionID error, connecting without ID...`);
|
|
134
|
+
this._setState(ConnectionState.Disconnected);
|
|
135
|
+
this.connect(true);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (isAPIError(err) && !isErrorRetryable(err)) {
|
|
140
|
+
this._setState(ConnectionState.Closed);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
await sleep(retryInterval(this.consecutiveFailures));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* connect try to open a longpoll request
|
|
151
|
+
* @param reconnect should be false for first call and true for subsequent calls to keep the connection alive and call recoverState
|
|
152
|
+
*/
|
|
153
|
+
connect = async (reconnect = false) => {
|
|
154
|
+
if (this.state === ConnectionState.Connecting) {
|
|
155
|
+
throw new Error('connecting already in progress');
|
|
156
|
+
}
|
|
157
|
+
if (this.state === ConnectionState.Connected) {
|
|
158
|
+
throw new Error('already connected and polling');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
this._setState(ConnectionState.Connecting);
|
|
162
|
+
this.connectionID = undefined; // connect should be sent with empty connection_id so API creates one
|
|
163
|
+
try {
|
|
164
|
+
const { event } = await this._req<{ event: ConnectionOpen<ChannelType, CommandType, UserType> }>(
|
|
165
|
+
{ json: this.client._buildWSPayload() },
|
|
166
|
+
{ timeout: 8000 }, // 8s
|
|
167
|
+
reconnect,
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
this._setState(ConnectionState.Connected);
|
|
171
|
+
this.connectionID = event.connection_id;
|
|
172
|
+
this._poll();
|
|
173
|
+
if (reconnect) {
|
|
174
|
+
this.client.recoverState();
|
|
175
|
+
}
|
|
176
|
+
return event;
|
|
177
|
+
} catch (err) {
|
|
178
|
+
this._setState(ConnectionState.Closed);
|
|
179
|
+
throw err;
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* isHealthy checks if there is a connectionID and connection is in Connected state
|
|
185
|
+
*/
|
|
186
|
+
isHealthy = () => {
|
|
187
|
+
return this.connectionID && this.state === ConnectionState.Connected;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
disconnect = async (timeout = 2000) => {
|
|
191
|
+
removeConnectionEventListeners(this._onlineStatusChanged);
|
|
192
|
+
|
|
193
|
+
this._setState(ConnectionState.Disconnected);
|
|
194
|
+
this.cancelToken?.cancel('disconnect() is called');
|
|
195
|
+
this.cancelToken = undefined;
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
await this._req({ close: true }, { timeout }, false);
|
|
199
|
+
this.connectionID = undefined;
|
|
200
|
+
this._log(`disconnect() - Closed connectionID`);
|
|
201
|
+
} catch (err) {
|
|
202
|
+
this._log(`disconnect() - Failed`, { err }, 'error');
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export const APIErrorCodes: Record<string, { name: string; retryable: boolean }> = {
|
|
2
|
+
'-1': { name: 'InternalSystemError', retryable: true },
|
|
3
|
+
'2': { name: 'AccessKeyError', retryable: false },
|
|
4
|
+
'3': { name: 'AuthenticationFailedError', retryable: true },
|
|
5
|
+
'4': { name: 'InputError', retryable: false },
|
|
6
|
+
'6': { name: 'DuplicateUsernameError', retryable: false },
|
|
7
|
+
'9': { name: 'RateLimitError', retryable: true },
|
|
8
|
+
'16': { name: 'DoesNotExistError', retryable: false },
|
|
9
|
+
'17': { name: 'NotAllowedError', retryable: false },
|
|
10
|
+
'18': { name: 'EventNotSupportedError', retryable: false },
|
|
11
|
+
'19': { name: 'ChannelFeatureNotSupportedError', retryable: false },
|
|
12
|
+
'20': { name: 'MessageTooLongError', retryable: false },
|
|
13
|
+
'21': { name: 'MultipleNestingLevelError', retryable: false },
|
|
14
|
+
'22': { name: 'PayloadTooBigError', retryable: false },
|
|
15
|
+
'23': { name: 'RequestTimeoutError', retryable: true },
|
|
16
|
+
'24': { name: 'MaxHeaderSizeExceededError', retryable: false },
|
|
17
|
+
'40': { name: 'AuthErrorTokenExpired', retryable: false },
|
|
18
|
+
'41': { name: 'AuthErrorTokenNotValidYet', retryable: false },
|
|
19
|
+
'42': { name: 'AuthErrorTokenUsedBeforeIssuedAt', retryable: false },
|
|
20
|
+
'43': { name: 'AuthErrorTokenSignatureInvalid', retryable: false },
|
|
21
|
+
'44': { name: 'CustomCommandEndpointMissingError', retryable: false },
|
|
22
|
+
'45': { name: 'CustomCommandEndpointCallError', retryable: true },
|
|
23
|
+
'46': { name: 'ConnectionIDNotFoundError', retryable: false },
|
|
24
|
+
'60': { name: 'CoolDownError', retryable: true },
|
|
25
|
+
'69': { name: 'ErrWrongRegion', retryable: false },
|
|
26
|
+
'70': { name: 'ErrQueryChannelPermissions', retryable: false },
|
|
27
|
+
'71': { name: 'ErrTooManyConnections', retryable: true },
|
|
28
|
+
'99': { name: 'AppSuspendedError', retryable: false },
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type APIError = Error & { code: number; isWSFailure?: boolean };
|
|
32
|
+
|
|
33
|
+
export function isAPIError(error: Error): error is APIError {
|
|
34
|
+
return (error as APIError).code !== undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function isErrorRetryable(error: APIError) {
|
|
38
|
+
if (!error.code) return false;
|
|
39
|
+
const err = APIErrorCodes[`${error.code}`];
|
|
40
|
+
if (!err) return false;
|
|
41
|
+
return err.retryable;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function isConnectionIDError(error: APIError) {
|
|
45
|
+
return error.code === 46; // ConnectionIDNotFoundError
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function isWSFailure(err: APIError): boolean {
|
|
49
|
+
if (typeof err.isWSFailure === 'boolean') {
|
|
50
|
+
return err.isWSFailure;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(err.message).isWSFailure;
|
|
55
|
+
} catch (_) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
package/src/insights.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
1
2
|
import { StableWSConnection } from './connection';
|
|
2
|
-
import {
|
|
3
|
-
import { randomId } from './utils';
|
|
3
|
+
import { randomId, sleep } from './utils';
|
|
4
4
|
|
|
5
|
-
export type InsightTypes = 'ws_fatal' | 'ws_success_after_failure' | '
|
|
5
|
+
export type InsightTypes = 'ws_fatal' | 'ws_success_after_failure' | 'http_hi_failed';
|
|
6
6
|
export class InsightMetrics {
|
|
7
7
|
connectionStartTimestamp: number | null;
|
|
8
8
|
wsConsecutiveFailures: number;
|
|
@@ -17,51 +17,57 @@ export class InsightMetrics {
|
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
) {
|
|
20
|
+
/**
|
|
21
|
+
* postInsights is not supposed to be used by end users directly within chat application, and thus is kept isolated
|
|
22
|
+
* from all the client/connection code/logic.
|
|
23
|
+
*
|
|
24
|
+
* @param insightType
|
|
25
|
+
* @param insights
|
|
26
|
+
*/
|
|
27
|
+
export const postInsights = async (insightType: InsightTypes, insights: Record<string, unknown>) => {
|
|
28
|
+
const maxAttempts = 3;
|
|
29
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
30
|
+
try {
|
|
31
|
+
await axios.post(`https://chat-insights.getstream.io/insights/${insightType}`, insights);
|
|
32
|
+
} catch (e) {
|
|
33
|
+
await sleep((i + 1) * 3000);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export function buildWsFatalInsight(connection: StableWSConnection, event: Record<string, unknown>) {
|
|
28
41
|
return {
|
|
29
42
|
...event,
|
|
30
43
|
...buildWsBaseInsight(connection),
|
|
31
44
|
};
|
|
32
45
|
}
|
|
33
46
|
|
|
34
|
-
function buildWsBaseInsight
|
|
35
|
-
|
|
36
|
-
CommandType extends string = LiteralStringForUnion,
|
|
37
|
-
UserType extends UnknownType = UnknownType
|
|
38
|
-
>(connection: StableWSConnection<ChannelType, CommandType, UserType>) {
|
|
47
|
+
function buildWsBaseInsight(connection: StableWSConnection) {
|
|
48
|
+
const { client } = connection;
|
|
39
49
|
return {
|
|
40
50
|
ready_state: connection.ws?.readyState,
|
|
41
|
-
url: connection._buildUrl(
|
|
42
|
-
api_key:
|
|
43
|
-
start_ts:
|
|
51
|
+
url: connection._buildUrl(),
|
|
52
|
+
api_key: client.key,
|
|
53
|
+
start_ts: client.insightMetrics.connectionStartTimestamp,
|
|
44
54
|
end_ts: new Date().getTime(),
|
|
45
|
-
auth_type:
|
|
46
|
-
token:
|
|
47
|
-
user_id:
|
|
48
|
-
user_details:
|
|
49
|
-
device:
|
|
55
|
+
auth_type: client.getAuthType(),
|
|
56
|
+
token: client.tokenManager.token,
|
|
57
|
+
user_id: client.userID,
|
|
58
|
+
user_details: client._user,
|
|
59
|
+
device: client.options.device,
|
|
50
60
|
client_id: connection.connectionID,
|
|
51
61
|
ws_details: connection.ws,
|
|
52
|
-
ws_consecutive_failures:
|
|
53
|
-
ws_total_failures:
|
|
62
|
+
ws_consecutive_failures: client.insightMetrics.wsConsecutiveFailures,
|
|
63
|
+
ws_total_failures: client.insightMetrics.wsTotalFailures,
|
|
54
64
|
request_id: connection.requestID,
|
|
55
65
|
online: typeof navigator !== 'undefined' ? navigator?.onLine : null,
|
|
56
66
|
user_agent: typeof navigator !== 'undefined' ? navigator?.userAgent : null,
|
|
57
|
-
instance_client_id:
|
|
67
|
+
instance_client_id: client.insightMetrics.instanceClientId,
|
|
58
68
|
};
|
|
59
69
|
}
|
|
60
70
|
|
|
61
|
-
export function buildWsSuccessAfterFailureInsight
|
|
62
|
-
ChannelType extends UnknownType = UnknownType,
|
|
63
|
-
CommandType extends string = LiteralStringForUnion,
|
|
64
|
-
UserType extends UnknownType = UnknownType
|
|
65
|
-
>(connection: StableWSConnection<ChannelType, CommandType, UserType>) {
|
|
71
|
+
export function buildWsSuccessAfterFailureInsight(connection: StableWSConnection) {
|
|
66
72
|
return buildWsBaseInsight(connection);
|
|
67
73
|
}
|
package/src/permissions.ts
CHANGED
|
@@ -37,33 +37,12 @@ export class Permission {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
// deprecated
|
|
40
|
-
export const AllowAll = new Permission(
|
|
41
|
-
'Allow all',
|
|
42
|
-
MaxPriority,
|
|
43
|
-
AnyResource,
|
|
44
|
-
AnyRole,
|
|
45
|
-
false,
|
|
46
|
-
Allow,
|
|
47
|
-
);
|
|
40
|
+
export const AllowAll = new Permission('Allow all', MaxPriority, AnyResource, AnyRole, false, Allow);
|
|
48
41
|
|
|
49
42
|
// deprecated
|
|
50
|
-
export const DenyAll = new Permission(
|
|
51
|
-
'Deny all',
|
|
52
|
-
MinPriority,
|
|
53
|
-
AnyResource,
|
|
54
|
-
AnyRole,
|
|
55
|
-
false,
|
|
56
|
-
Deny,
|
|
57
|
-
);
|
|
43
|
+
export const DenyAll = new Permission('Deny all', MinPriority, AnyResource, AnyRole, false, Deny);
|
|
58
44
|
|
|
59
|
-
export type Role =
|
|
60
|
-
| 'admin'
|
|
61
|
-
| 'user'
|
|
62
|
-
| 'guest'
|
|
63
|
-
| 'anonymous'
|
|
64
|
-
| 'channel_member'
|
|
65
|
-
| 'channel_moderator'
|
|
66
|
-
| string;
|
|
45
|
+
export type Role = 'admin' | 'user' | 'guest' | 'anonymous' | 'channel_member' | 'channel_moderator' | string;
|
|
67
46
|
|
|
68
47
|
export const BuiltinRoles = {
|
|
69
48
|
Admin: 'admin',
|
package/src/signing.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import jwt, { Secret, SignOptions } from 'jsonwebtoken';
|
|
2
2
|
import crypto from 'crypto';
|
|
3
3
|
import { encodeBase64, decodeBase64 } from './base64';
|
|
4
|
-
import {
|
|
4
|
+
import { UR } from './types';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Creates the JWT token that can be used for a UserSession
|
|
@@ -10,21 +10,16 @@ import { UnknownType } from './types';
|
|
|
10
10
|
* @private
|
|
11
11
|
* @param {Secret} apiSecret - API Secret key
|
|
12
12
|
* @param {string} userId - The user_id key in the JWT payload
|
|
13
|
-
* @param {
|
|
13
|
+
* @param {UR} [extraData] - Extra that should be part of the JWT token
|
|
14
14
|
* @param {SignOptions} [jwtOptions] - Options that can be past to jwt.sign
|
|
15
15
|
* @return {string} JWT Token
|
|
16
16
|
*/
|
|
17
|
-
export function JWTUserToken(
|
|
18
|
-
apiSecret: Secret,
|
|
19
|
-
userId: string,
|
|
20
|
-
extraData: UnknownType = {},
|
|
21
|
-
jwtOptions: SignOptions = {},
|
|
22
|
-
) {
|
|
17
|
+
export function JWTUserToken(apiSecret: Secret, userId: string, extraData: UR = {}, jwtOptions: SignOptions = {}) {
|
|
23
18
|
if (typeof userId !== 'string') {
|
|
24
19
|
throw new TypeError('userId should be a string');
|
|
25
20
|
}
|
|
26
21
|
|
|
27
|
-
const payload: { user_id: string } &
|
|
22
|
+
const payload: { user_id: string } & UR = {
|
|
28
23
|
user_id: userId,
|
|
29
24
|
...extraData,
|
|
30
25
|
};
|
|
@@ -36,10 +31,7 @@ export function JWTUserToken(
|
|
|
36
31
|
);
|
|
37
32
|
}
|
|
38
33
|
|
|
39
|
-
const opts: SignOptions = Object.assign(
|
|
40
|
-
{ algorithm: 'HS256', noTimestamp: true },
|
|
41
|
-
jwtOptions,
|
|
42
|
-
);
|
|
34
|
+
const opts: SignOptions = Object.assign({ algorithm: 'HS256', noTimestamp: true }, jwtOptions);
|
|
43
35
|
|
|
44
36
|
if (payload.iat) {
|
|
45
37
|
opts.noTimestamp = false;
|
|
@@ -52,10 +44,7 @@ export function JWTServerToken(apiSecret: Secret, jwtOptions: SignOptions = {})
|
|
|
52
44
|
server: true,
|
|
53
45
|
};
|
|
54
46
|
|
|
55
|
-
const opts: SignOptions = Object.assign(
|
|
56
|
-
{ algorithm: 'HS256', noTimestamp: true },
|
|
57
|
-
jwtOptions,
|
|
58
|
-
);
|
|
47
|
+
const opts: SignOptions = Object.assign({ algorithm: 'HS256', noTimestamp: true }, jwtOptions);
|
|
59
48
|
return jwt.sign(payload, apiSecret, opts);
|
|
60
49
|
}
|
|
61
50
|
|
package/src/token_manager.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { Secret } from 'jsonwebtoken';
|
|
2
2
|
import { UserFromToken, JWTServerToken, JWTUserToken } from './signing';
|
|
3
3
|
import { isFunction } from './utils';
|
|
4
|
-
import { TokenOrProvider,
|
|
4
|
+
import { TokenOrProvider, UR, UserResponse } from './types';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* TokenManager
|
|
8
8
|
*
|
|
9
9
|
* Handles all the operations around user token.
|
|
10
10
|
*/
|
|
11
|
-
export class TokenManager<UserType extends
|
|
11
|
+
export class TokenManager<UserType extends UR = UR> {
|
|
12
12
|
loadTokenPromise: Promise<string> | null;
|
|
13
13
|
type: 'static' | 'provider';
|
|
14
14
|
secret?: Secret;
|
|
@@ -40,10 +40,7 @@ export class TokenManager<UserType extends UnknownType = UnknownType> {
|
|
|
40
40
|
* @param {TokenOrProvider} tokenOrProvider
|
|
41
41
|
* @param {UserResponse<UserType>} user
|
|
42
42
|
*/
|
|
43
|
-
setTokenOrProvider = async (
|
|
44
|
-
tokenOrProvider: TokenOrProvider,
|
|
45
|
-
user: UserResponse<UserType>,
|
|
46
|
-
) => {
|
|
43
|
+
setTokenOrProvider = async (tokenOrProvider: TokenOrProvider, user: UserResponse<UserType>) => {
|
|
47
44
|
this.validateToken(tokenOrProvider, user);
|
|
48
45
|
this.user = user;
|
|
49
46
|
|
|
@@ -85,11 +82,7 @@ export class TokenManager<UserType extends UnknownType = UnknownType> {
|
|
|
85
82
|
throw new Error('User token can not be empty');
|
|
86
83
|
}
|
|
87
84
|
|
|
88
|
-
if (
|
|
89
|
-
tokenOrProvider &&
|
|
90
|
-
typeof tokenOrProvider !== 'string' &&
|
|
91
|
-
!isFunction(tokenOrProvider)
|
|
92
|
-
) {
|
|
85
|
+
if (tokenOrProvider && typeof tokenOrProvider !== 'string' && !isFunction(tokenOrProvider)) {
|
|
93
86
|
throw new Error('user token should either be a string or a function');
|
|
94
87
|
}
|
|
95
88
|
|
|
@@ -98,13 +91,8 @@ export class TokenManager<UserType extends UnknownType = UnknownType> {
|
|
|
98
91
|
if (user.anon && tokenOrProvider === '') return;
|
|
99
92
|
|
|
100
93
|
const tokenUserId = UserFromToken(tokenOrProvider);
|
|
101
|
-
if (
|
|
102
|
-
|
|
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
|
-
);
|
|
94
|
+
if (tokenOrProvider != null && (tokenUserId == null || tokenUserId === '' || tokenUserId !== user.id)) {
|
|
95
|
+
throw new Error('userToken does not have a user_id or is not matching with user.id');
|
|
108
96
|
}
|
|
109
97
|
}
|
|
110
98
|
};
|