@sogni-ai/sogni-client 0.4.2 → 0.5.0-alpha.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.
Files changed (38) hide show
  1. package/dist/Account/CurrentAccount.d.ts +4 -6
  2. package/dist/Account/CurrentAccount.js +3 -30
  3. package/dist/Account/CurrentAccount.js.map +1 -1
  4. package/dist/Account/index.d.ts +5 -3
  5. package/dist/Account/index.js +20 -9
  6. package/dist/Account/index.js.map +1 -1
  7. package/dist/Account/types.d.ts +2 -0
  8. package/dist/ApiClient/WebSocketClient/index.d.ts +4 -4
  9. package/dist/ApiClient/WebSocketClient/index.js +32 -49
  10. package/dist/ApiClient/WebSocketClient/index.js.map +1 -1
  11. package/dist/ApiClient/index.d.ts +4 -10
  12. package/dist/ApiClient/index.js +26 -17
  13. package/dist/ApiClient/index.js.map +1 -1
  14. package/dist/Projects/index.js +8 -5
  15. package/dist/Projects/index.js.map +1 -1
  16. package/dist/lib/AuthManager.d.ts +51 -0
  17. package/dist/lib/AuthManager.js +157 -0
  18. package/dist/lib/AuthManager.js.map +1 -0
  19. package/dist/lib/RestClient.d.ts +4 -7
  20. package/dist/lib/RestClient.js +7 -7
  21. package/dist/lib/RestClient.js.map +1 -1
  22. package/dist/lib/utils.d.ts +8 -0
  23. package/dist/lib/utils.js +20 -0
  24. package/dist/lib/utils.js.map +1 -0
  25. package/dist/version.d.ts +1 -1
  26. package/dist/version.js +1 -1
  27. package/dist/version.js.map +1 -1
  28. package/package.json +1 -1
  29. package/src/Account/CurrentAccount.ts +6 -35
  30. package/src/Account/index.ts +18 -8
  31. package/src/Account/types.ts +2 -0
  32. package/src/ApiClient/WebSocketClient/index.ts +13 -24
  33. package/src/ApiClient/index.ts +19 -27
  34. package/src/Projects/index.ts +1 -1
  35. package/src/lib/AuthManager.ts +172 -0
  36. package/src/lib/RestClient.ts +8 -13
  37. package/src/lib/utils.ts +17 -0
  38. package/src/version.ts +1 -1
@@ -1,13 +1,13 @@
1
1
  import { MessageType, SocketMessageMap } from './messages';
2
2
  import { SocketEventMap } from './events';
3
- import RestClient, { AuthData } from '../../lib/RestClient';
3
+ import RestClient from '../../lib/RestClient';
4
4
  import { SupernetType } from './types';
5
5
  import WebSocket, { CloseEvent, ErrorEvent, MessageEvent } from 'isomorphic-ws';
6
6
  import { base64Decode, base64Encode } from '../../lib/base64';
7
7
  import isNodejs from '../../lib/isNodejs';
8
- import Cookie from 'js-cookie';
9
8
  import { LIB_VERSION } from '../../version';
10
9
  import { Logger } from '../../lib/DefaultLogger';
10
+ import AuthManager from '../../lib/AuthManager';
11
11
 
12
12
  const PING_INTERVAL = 15000;
13
13
 
@@ -18,34 +18,23 @@ class WebSocketClient extends RestClient<SocketEventMap> {
18
18
  private _supernetType: SupernetType;
19
19
  private _pingInterval: NodeJS.Timeout | null = null;
20
20
 
21
- constructor(baseUrl: string, appId: string, supernetType: SupernetType, logger: Logger) {
21
+ constructor(
22
+ baseUrl: string,
23
+ auth: AuthManager,
24
+ appId: string,
25
+ supernetType: SupernetType,
26
+ logger: Logger
27
+ ) {
22
28
  const _baseUrl = new URL(baseUrl);
23
29
  if (_baseUrl.protocol === 'wss:') {
24
30
  _baseUrl.protocol = 'https:';
25
31
  }
26
- super(_baseUrl.toString(), logger);
32
+ super(_baseUrl.toString(), auth, logger);
27
33
  this.appId = appId;
28
34
  this.baseUrl = _baseUrl.toString();
29
35
  this._supernetType = supernetType;
30
36
  }
31
37
 
32
- set auth(auth: AuthData | null) {
33
- //In browser, set the cookie
34
- if (!isNodejs) {
35
- if (auth) {
36
- Cookie.set('authorization', auth.token, {
37
- domain: '.sogni.ai',
38
- expires: 1
39
- });
40
- } else {
41
- Cookie.remove('authorization', {
42
- domain: '.sogni.ai'
43
- });
44
- }
45
- }
46
- this._auth = auth;
47
- }
48
-
49
38
  get supernetType(): SupernetType {
50
39
  return this._supernetType;
51
40
  }
@@ -54,7 +43,7 @@ class WebSocketClient extends RestClient<SocketEventMap> {
54
43
  return !!this.socket;
55
44
  }
56
45
 
57
- connect() {
46
+ async connect() {
58
47
  if (this.socket) {
59
48
  this.disconnect();
60
49
  }
@@ -71,7 +60,7 @@ class WebSocketClient extends RestClient<SocketEventMap> {
71
60
  if (isNodejs) {
72
61
  params = {
73
62
  headers: {
74
- Authorization: this._auth?.token,
63
+ Authorization: await this.auth.getToken(),
75
64
  'User-Agent': userAgent
76
65
  }
77
66
  };
@@ -114,7 +103,7 @@ class WebSocketClient extends RestClient<SocketEventMap> {
114
103
  }
115
104
 
116
105
  switchNetwork(supernetType: SupernetType): Promise<SupernetType> {
117
- return new Promise<SupernetType>(async (resolve, reject) => {
106
+ return new Promise<SupernetType>(async (resolve) => {
118
107
  this.once('changeNetwork', ({ network }) => {
119
108
  this._supernetType = network;
120
109
  resolve(network);
@@ -1,6 +1,5 @@
1
1
  import RestClient from '../lib/RestClient';
2
2
  import WebSocketClient from './WebSocketClient';
3
- import { jwtDecode } from 'jwt-decode';
4
3
  import TypedEventEmitter from '../lib/TypedEventEmitter';
5
4
  import { ApiClientEvents } from './events';
6
5
  import { ServerConnectData, ServerDisconnectData } from './WebSocketClient/events';
@@ -8,6 +7,7 @@ import { isNotRecoverable } from './WebSocketClient/ErrorCode';
8
7
  import { JSONValue } from '../types/json';
9
8
  import { SupernetType } from './WebSocketClient/types';
10
9
  import { Logger } from '../lib/DefaultLogger';
10
+ import AuthManager, { Tokens } from '../lib/AuthManager';
11
11
 
12
12
  const WS_RECONNECT_ATTEMPTS = 5;
13
13
 
@@ -33,21 +33,12 @@ export class ApiError extends Error {
33
33
  }
34
34
  }
35
35
 
36
- /**
37
- * @inline
38
- */
39
- interface AuthData {
40
- token: string;
41
- walletAddress: string;
42
- expiresAt: Date;
43
- }
44
-
45
36
  class ApiClient extends TypedEventEmitter<ApiClientEvents> {
46
37
  readonly appId: string;
47
38
  readonly logger: Logger;
48
39
  private _rest: RestClient;
49
40
  private _socket: WebSocketClient;
50
- private _auth: AuthData | null = null;
41
+ private _auth: AuthManager;
51
42
  private _reconnectAttempts = WS_RECONNECT_ATTEMPTS;
52
43
 
53
44
  constructor(
@@ -60,18 +51,21 @@ class ApiClient extends TypedEventEmitter<ApiClientEvents> {
60
51
  super();
61
52
  this.appId = appId;
62
53
  this.logger = logger;
63
- this._rest = new RestClient(baseUrl, logger);
64
- this._socket = new WebSocketClient(socketUrl, appId, networkType, logger);
54
+ this._auth = new AuthManager(baseUrl, logger);
55
+ this._rest = new RestClient(baseUrl, this._auth, logger);
56
+ this._socket = new WebSocketClient(socketUrl, this._auth, appId, networkType, logger);
57
+
58
+ this._auth.on('refreshFailed', this.handleRefreshFailed.bind(this));
65
59
  this._socket.on('connected', this.handleSocketConnect.bind(this));
66
60
  this._socket.on('disconnected', this.handleSocketDisconnect.bind(this));
67
61
  }
68
62
 
69
63
  get isAuthenticated(): boolean {
70
- return !!this._auth && this._auth.expiresAt > new Date();
64
+ return this.auth.isAuthenticated;
71
65
  }
72
66
 
73
- get auth(): AuthData | null {
74
- return this._auth && this._auth.expiresAt > new Date() ? this._auth : null;
67
+ get auth(): AuthManager {
68
+ return this._auth;
75
69
  }
76
70
 
77
71
  get socket(): WebSocketClient {
@@ -82,20 +76,13 @@ class ApiClient extends TypedEventEmitter<ApiClientEvents> {
82
76
  return this._rest;
83
77
  }
84
78
 
85
- authenticate(token: string) {
86
- const decoded = jwtDecode<{ addr: string; env: string; iat: number; exp: number }>(token);
87
- this._auth = {
88
- token,
89
- walletAddress: decoded.addr,
90
- expiresAt: new Date(decoded.exp * 1000)
91
- };
92
- this.rest.auth = { token };
93
- this.socket.auth = { token };
94
- this.socket.connect();
79
+ async authenticate(tokens: Tokens) {
80
+ await this.auth.setTokens(tokens);
81
+ await this.socket.connect();
95
82
  }
96
83
 
97
84
  removeAuth() {
98
- this._auth = null;
85
+ this.auth.clear();
99
86
  this.socket.disconnect();
100
87
  }
101
88
 
@@ -112,6 +99,7 @@ class ApiClient extends TypedEventEmitter<ApiClientEvents> {
112
99
  return;
113
100
  }
114
101
  if (this._reconnectAttempts <= 0) {
102
+ this.removeAuth();
115
103
  this.emit('disconnected', data);
116
104
  this._reconnectAttempts = WS_RECONNECT_ATTEMPTS;
117
105
  return;
@@ -119,6 +107,10 @@ class ApiClient extends TypedEventEmitter<ApiClientEvents> {
119
107
  this._reconnectAttempts--;
120
108
  setTimeout(() => this.socket.connect(), 1000);
121
109
  }
110
+
111
+ handleRefreshFailed() {
112
+ this.removeAuth();
113
+ }
122
114
  }
123
115
 
124
116
  export default ApiClient;
@@ -72,7 +72,7 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
72
72
  }, {});
73
73
  this._availableModels = Object.entries(data).map(([id, workerCount]) => ({
74
74
  id,
75
- name: modelIndex[id].modelShortName || id.replace(/-/g, ' '),
75
+ name: modelIndex[id]?.modelShortName || id.replace(/-/g, ' '),
76
76
  workerCount
77
77
  }));
78
78
  this.emit('availableModels', this._availableModels);
@@ -0,0 +1,172 @@
1
+ import { decodeToken, decodeRefreshToken } from './utils';
2
+ import { ApiError, ApiErrorResponse } from '../ApiClient';
3
+ import { Logger } from './DefaultLogger';
4
+ import isNodejs from './isNodejs';
5
+ import Cookie from 'js-cookie';
6
+ import TypedEventEmitter from './TypedEventEmitter';
7
+
8
+ /**
9
+ * Tokens object, containing the token and refresh token
10
+ * @typedef {Object} Tokens
11
+ * @property {string} [token] - The JWT token. Optonal, if missing it will be retrieved from the server
12
+ * @property {string} refreshToken - The refresh token
13
+ */
14
+ export interface Tokens {
15
+ token?: string;
16
+ refreshToken: string;
17
+ }
18
+
19
+ export type AuthUpdatedEvent =
20
+ | { token: string; refreshToken: string; walletAddress: string }
21
+ | { token: null; refreshToken: null; walletAddress: null };
22
+
23
+ interface AuthManagerEvents {
24
+ updated: AuthUpdatedEvent;
25
+ refreshFailed: ApiErrorResponse;
26
+ }
27
+
28
+ class AuthManager extends TypedEventEmitter<AuthManagerEvents> {
29
+ private _token?: string;
30
+ private _tokenExpiresAt: Date = new Date(0);
31
+ private _refreshToken?: string;
32
+ private _refreshTokenExpiresAt: Date = new Date(0);
33
+ private _logger: Logger;
34
+ private _baseUrl: string;
35
+ private _walletAddress?: string;
36
+ private _renewTokenPromise?: Promise<string>;
37
+
38
+ constructor(baseUrl: string, logger: Logger) {
39
+ super();
40
+ this._logger = logger;
41
+ this._baseUrl = baseUrl;
42
+ }
43
+
44
+ get refreshToken() {
45
+ return this._refreshToken;
46
+ }
47
+
48
+ get walletAddress() {
49
+ return this._walletAddress;
50
+ }
51
+
52
+ get isAuthenticated() {
53
+ return !!this._refreshToken && this._refreshTokenExpiresAt > new Date();
54
+ }
55
+
56
+ private _updateCookies() {
57
+ if (isNodejs) {
58
+ return;
59
+ }
60
+ const token = this._token;
61
+ if (token) {
62
+ Cookie.set('authorization', token, {
63
+ domain: '.sogni.ai',
64
+ expires: 1
65
+ });
66
+ } else {
67
+ Cookie.remove('authorization', {
68
+ domain: '.sogni.ai'
69
+ });
70
+ }
71
+ }
72
+
73
+ async setTokens({ refreshToken, token }: { refreshToken: string; token?: string }) {
74
+ if (token) {
75
+ this._updateTokens({ token, refreshToken });
76
+ return;
77
+ }
78
+ this._refreshToken = refreshToken;
79
+ const { expiresAt: refreshExpiresAt } = decodeRefreshToken(refreshToken);
80
+ this._refreshTokenExpiresAt = refreshExpiresAt;
81
+ await this.renewToken();
82
+ }
83
+
84
+ async getToken(): Promise<string | undefined> {
85
+ //If there is a token, and it is not expired, return it
86
+ if (this._token && this._tokenExpiresAt > new Date()) {
87
+ return this._token;
88
+ }
89
+ //If there is no refresh token, return undefined, to make unauthorized requests
90
+ if (!this._refreshToken) {
91
+ return;
92
+ }
93
+ //If there is a refresh token, try to renew the token
94
+ return this.renewToken();
95
+ }
96
+
97
+ async renewToken(): Promise<string> {
98
+ if (this._renewTokenPromise) {
99
+ return this._renewTokenPromise;
100
+ }
101
+ this._renewTokenPromise = this._renewToken();
102
+ this._renewTokenPromise.finally(() => {
103
+ this._renewTokenPromise = undefined;
104
+ });
105
+ return this._renewTokenPromise;
106
+ }
107
+
108
+ clear() {
109
+ // Prevent duplicate events
110
+ if (!this._token && !this._refreshToken) {
111
+ return;
112
+ }
113
+ this._refreshToken = undefined;
114
+ this._refreshTokenExpiresAt = new Date(0);
115
+ this._token = undefined;
116
+ this._tokenExpiresAt = new Date(0);
117
+ this._walletAddress = undefined;
118
+ this.emit('updated', { token: null, refreshToken: null, walletAddress: null });
119
+ }
120
+
121
+ private _updateTokens({ token, refreshToken }: { token: string; refreshToken: string }) {
122
+ // Prevent duplicate events
123
+ if (this._token === token && this._refreshToken === refreshToken) {
124
+ return;
125
+ }
126
+ this._token = token;
127
+ const { expiresAt, walletAddress } = decodeToken(token);
128
+ this._walletAddress = walletAddress;
129
+ this._tokenExpiresAt = expiresAt;
130
+ this._refreshToken = refreshToken;
131
+ const { expiresAt: refreshExpiresAt } = decodeRefreshToken(refreshToken);
132
+ this._refreshTokenExpiresAt = refreshExpiresAt;
133
+ this._updateCookies();
134
+ this.emit('updated', { token, refreshToken, walletAddress });
135
+ }
136
+
137
+ private async _renewToken(): Promise<string> {
138
+ if (this._refreshTokenExpiresAt < new Date()) {
139
+ throw new Error('Refresh token expired');
140
+ }
141
+ const url = new URL('/v1/account/refresh-token', this._baseUrl).toString();
142
+ const response = await fetch(url, {
143
+ method: 'POST',
144
+ headers: {
145
+ 'Content-Type': 'application/json'
146
+ },
147
+ body: JSON.stringify({ refreshToken: this._refreshToken })
148
+ });
149
+ let responseData: any;
150
+ try {
151
+ responseData = await response.json();
152
+ } catch (e) {
153
+ this.emit('refreshFailed', {
154
+ status: 'error',
155
+ errorCode: 0,
156
+ message: 'Failed to parse response'
157
+ });
158
+ this._logger.error('Failed to parse response:', e);
159
+ throw new Error('Failed to parse response');
160
+ }
161
+ if (!response.ok) {
162
+ this.emit('refreshFailed', responseData);
163
+ this.clear();
164
+ throw new ApiError(response.status, responseData as ApiErrorResponse);
165
+ }
166
+ const { token, refreshToken } = responseData.data;
167
+ this._updateTokens({ token, refreshToken });
168
+ return this._token!;
169
+ }
170
+ }
171
+
172
+ export default AuthManager;
@@ -2,30 +2,24 @@ import { ApiError, ApiErrorResponse } from '../ApiClient';
2
2
  import TypedEventEmitter, { EventMap } from './TypedEventEmitter';
3
3
  import { JSONValue } from '../types/json';
4
4
  import { Logger } from './DefaultLogger';
5
-
6
- export interface AuthData {
7
- token: string;
8
- }
5
+ import AuthManager from './AuthManager';
9
6
 
10
7
  class RestClient<E extends EventMap = never> extends TypedEventEmitter<E> {
11
8
  readonly baseUrl: string;
12
- protected _auth: AuthData | null = null;
9
+ protected _auth: AuthManager;
13
10
  protected _logger: Logger;
14
11
 
15
- constructor(baseUrl: string, logger: Logger) {
12
+ constructor(baseUrl: string, auth: AuthManager, logger: Logger) {
16
13
  super();
17
14
  this.baseUrl = baseUrl;
15
+ this._auth = auth;
18
16
  this._logger = logger;
19
17
  }
20
18
 
21
- get auth(): AuthData | null {
19
+ get auth(): AuthManager {
22
20
  return this._auth;
23
21
  }
24
22
 
25
- set auth(auth: AuthData | null) {
26
- this._auth = auth;
27
- }
28
-
29
23
  private formatUrl(relativeUrl: string, query: Record<string, string> = {}): string {
30
24
  const url = new URL(relativeUrl, this.baseUrl);
31
25
  Object.keys(query).forEach((key) => {
@@ -34,12 +28,13 @@ class RestClient<E extends EventMap = never> extends TypedEventEmitter<E> {
34
28
  return url.toString();
35
29
  }
36
30
 
37
- private request<T = JSONValue>(url: string, options: RequestInit = {}): Promise<T> {
31
+ private async request<T = JSONValue>(url: string, options: RequestInit = {}): Promise<T> {
32
+ const token = await this.auth.getToken();
38
33
  return fetch(url, {
39
34
  ...options,
40
35
  headers: {
41
36
  ...(options.headers || {}),
42
- ...(this.auth ? { Authorization: this.auth.token } : {})
37
+ ...(token ? { Authorization: token } : {})
43
38
  }
44
39
  }).then((r) => this.processResponse(r) as T);
45
40
  }
@@ -0,0 +1,17 @@
1
+ import { jwtDecode } from 'jwt-decode';
2
+
3
+ export function decodeToken(token: string) {
4
+ const data = jwtDecode<{ addr: string; env: string; iat: number; exp: number }>(token);
5
+ return {
6
+ walletAddress: data.addr,
7
+ expiresAt: new Date(data.exp * 1000)
8
+ };
9
+ }
10
+
11
+ export function decodeRefreshToken(token: string) {
12
+ const data = jwtDecode<{ type: string; env: string; iat: number; exp: number }>(token);
13
+ return {
14
+ env: data.env,
15
+ expiresAt: new Date(data.exp * 1000)
16
+ };
17
+ }
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const LIB_VERSION = "0.4.2";
1
+ export const LIB_VERSION = "0.5.0-alpha.0";