@sogni-ai/sogni-client 3.3.0 → 4.0.0-alpha.1

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 (46) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/Account/CurrentAccount.d.ts +2 -4
  3. package/dist/Account/CurrentAccount.js +4 -9
  4. package/dist/Account/CurrentAccount.js.map +1 -1
  5. package/dist/Account/index.d.ts +2 -30
  6. package/dist/Account/index.js +35 -46
  7. package/dist/Account/index.js.map +1 -1
  8. package/dist/Account/types.d.ts +11 -0
  9. package/dist/ApiClient/WebSocketClient/index.d.ts +1 -1
  10. package/dist/ApiClient/WebSocketClient/index.js +1 -10
  11. package/dist/ApiClient/WebSocketClient/index.js.map +1 -1
  12. package/dist/ApiClient/index.d.ts +12 -5
  13. package/dist/ApiClient/index.js +17 -31
  14. package/dist/ApiClient/index.js.map +1 -1
  15. package/dist/index.d.ts +23 -0
  16. package/dist/index.js +48 -1
  17. package/dist/index.js.map +1 -1
  18. package/dist/lib/AuthManager/AuthManagerBase.d.ts +25 -0
  19. package/dist/lib/AuthManager/AuthManagerBase.js +14 -0
  20. package/dist/lib/AuthManager/AuthManagerBase.js.map +1 -0
  21. package/dist/lib/AuthManager/CookieAuthManager.d.ts +15 -0
  22. package/dist/lib/AuthManager/CookieAuthManager.js +53 -0
  23. package/dist/lib/AuthManager/CookieAuthManager.js.map +1 -0
  24. package/dist/lib/AuthManager/TokenAuthManager.d.ts +41 -0
  25. package/dist/lib/{AuthManager.js → AuthManager/TokenAuthManager.js} +96 -60
  26. package/dist/lib/AuthManager/TokenAuthManager.js.map +1 -0
  27. package/dist/lib/AuthManager/index.d.ts +5 -0
  28. package/dist/lib/AuthManager/index.js +11 -0
  29. package/dist/lib/AuthManager/index.js.map +1 -0
  30. package/dist/lib/RestClient.d.ts +1 -1
  31. package/dist/lib/RestClient.js +6 -2
  32. package/dist/lib/RestClient.js.map +1 -1
  33. package/package.json +1 -1
  34. package/src/Account/CurrentAccount.ts +5 -12
  35. package/src/Account/index.ts +33 -45
  36. package/src/Account/types.ts +12 -0
  37. package/src/ApiClient/WebSocketClient/index.ts +2 -11
  38. package/src/ApiClient/index.ts +35 -31
  39. package/src/index.ts +63 -9
  40. package/src/lib/AuthManager/AuthManagerBase.ts +37 -0
  41. package/src/lib/AuthManager/CookieAuthManager.ts +40 -0
  42. package/src/lib/{AuthManager.ts → AuthManager/TokenAuthManager.ts} +97 -77
  43. package/src/lib/AuthManager/index.ts +8 -0
  44. package/src/lib/RestClient.ts +7 -9
  45. package/dist/lib/AuthManager.d.ts +0 -51
  46. package/dist/lib/AuthManager.js.map +0 -1
@@ -5,6 +5,7 @@ import {
5
5
  ClaimOptions,
6
6
  FullBalances,
7
7
  LoginData,
8
+ MeData,
8
9
  Nonce,
9
10
  Reward,
10
11
  RewardRaw,
@@ -18,8 +19,8 @@ import { parseEther, pbkdf2, toUtf8Bytes, Wallet } from 'ethers';
18
19
  import { ApiError, ApiResponse } from '../ApiClient';
19
20
  import CurrentAccount from './CurrentAccount';
20
21
  import { SupernetType } from '../ApiClient/WebSocketClient/types';
21
- import { AuthUpdatedEvent, Tokens } from '../lib/AuthManager';
22
22
  import { delay } from '../lib/utils';
23
+ import { TokenAuthManager } from '../lib/AuthManager';
23
24
 
24
25
  const MAX_DEPOSIT_ATTEMPTS = 4;
25
26
  enum ErrorCode {
@@ -60,14 +61,15 @@ class AccountApi extends ApiGroup {
60
61
  }
61
62
 
62
63
  private handleServerDisconnected() {
63
- this.currentAccount._clear();
64
+ this.currentAccount._update({
65
+ networkStatus: 'disconnected',
66
+ network: null
67
+ });
64
68
  }
65
69
 
66
- private handleAuthUpdated({ refreshToken, token, walletAddress }: AuthUpdatedEvent) {
67
- if (!refreshToken) {
70
+ private handleAuthUpdated(isAuthenticated: boolean) {
71
+ if (!isAuthenticated) {
68
72
  this.currentAccount._clear();
69
- } else {
70
- this.currentAccount._update({ walletAddress, token, refreshToken });
71
73
  }
72
74
  }
73
75
 
@@ -132,45 +134,16 @@ class AccountApi extends ApiGroup {
132
134
  referralCode,
133
135
  signature
134
136
  });
135
- await this.setToken(username, { refreshToken: res.data.refreshToken, token: res.data.token });
137
+ const auth = this.client.auth;
138
+ if (auth instanceof TokenAuthManager) {
139
+ await auth.authenticate({ refreshToken: res.data.refreshToken, token: res.data.token });
140
+ } else {
141
+ await auth.authenticate();
142
+ }
143
+ await this.me();
136
144
  return res.data;
137
145
  }
138
146
 
139
- /**
140
- * Restore session with username and refresh token.
141
- *
142
- * You can save access token that you get from the login method and restore the session with this method.
143
- *
144
- * @example Store access token to local storage
145
- * ```typescript
146
- * const { username, token, refreshToken } = await client.account.login('username', 'password');
147
- * localStorage.setItem('sogni-username', username);
148
- * localStorage.setItem('sogni-token', token);
149
- * localStorage.setItem('sogni-refresh-token', refreshToken);
150
- * ```
151
- *
152
- * @example Restore session from local storage
153
- * ```typescript
154
- * const username = localStorage.getItem('sogni-username');
155
- * const token = localStorage.getItem('sogni-token');
156
- * const refreshToken = localStorage.getItem('sogni-refresh-token');
157
- * if (username && refreshToken) {
158
- * client.account.setToken(username, {token, refreshToken});
159
- * console.log('Session restored');
160
- * }
161
- * ```
162
- *
163
- * @param username
164
- * @param tokens - Refresh token, access token pair { refreshToken: string, token: string }
165
- */
166
- async setToken(username: string, tokens: Tokens): Promise<void> {
167
- await this.client.authenticate(tokens);
168
- this.currentAccount._update({
169
- username,
170
- walletAddress: this.client.auth.walletAddress
171
- });
172
- }
173
-
174
147
  /**
175
148
  * Login with username and password. WebSocket connection is established after successful login.
176
149
  *
@@ -194,7 +167,13 @@ class AccountApi extends ApiGroup {
194
167
  walletAddress: wallet.address,
195
168
  signature
196
169
  });
197
- await this.setToken(username, { refreshToken: res.data.refreshToken, token: res.data.token });
170
+ const auth = this.client.auth;
171
+ if (auth instanceof TokenAuthManager) {
172
+ await auth.authenticate({ refreshToken: res.data.refreshToken, token: res.data.token });
173
+ } else {
174
+ await auth.authenticate();
175
+ }
176
+ await this.me();
198
177
  return res.data;
199
178
  }
200
179
 
@@ -211,8 +190,7 @@ class AccountApi extends ApiGroup {
211
190
  this.client.rest.post('/v1/account/logout').catch((e) => {
212
191
  this.client.logger.error('Failed to logout', e);
213
192
  });
214
- this.client.removeAuth();
215
- this.currentAccount._clear();
193
+ this.client.auth.clear();
216
194
  }
217
195
 
218
196
  /**
@@ -273,6 +251,16 @@ class AccountApi extends ApiGroup {
273
251
  return res.data;
274
252
  }
275
253
 
254
+ async me() {
255
+ const res = await this.client.rest.get<ApiResponse<MeData>>('/v1/account/me');
256
+ this.currentAccount._update({
257
+ username: res.data.username,
258
+ email: res.data.currentEmail,
259
+ walletAddress: res.data.walletAddress
260
+ });
261
+ return res.data;
262
+ }
263
+
276
264
  /**
277
265
  * Validate the username before signup
278
266
  * @internal
@@ -24,6 +24,18 @@ export interface LoginData {
24
24
  username: string;
25
25
  }
26
26
 
27
+ export interface MeData {
28
+ currentEmail: string;
29
+ discord2FA: boolean;
30
+ discordLinked: boolean;
31
+ discordServerMember: boolean;
32
+ discordUsername: string;
33
+ emailVerified: boolean;
34
+ requestedUpdatedEmail: string;
35
+ username: string;
36
+ walletAddress: string;
37
+ }
38
+
27
39
  export interface BalanceData {
28
40
  settled: string;
29
41
  credit: string;
@@ -7,7 +7,7 @@ import { base64Decode, base64Encode } from '../../lib/base64';
7
7
  import isNodejs from '../../lib/isNodejs';
8
8
  import { LIB_VERSION } from '../../version';
9
9
  import { Logger } from '../../lib/DefaultLogger';
10
- import AuthManager from '../../lib/AuthManager';
10
+ import { AuthManager } from '../../lib/AuthManager';
11
11
 
12
12
  const PROTOCOL_VERSION = '3.0.0';
13
13
 
@@ -67,16 +67,7 @@ class WebSocketClient extends RestClient<SocketEventMap> {
67
67
  url.searchParams.set('clientType', 'artist');
68
68
  //At this point 'relaxed' does not work as expected, so we use 'fast' or empty
69
69
  url.searchParams.set('forceWorkerId', this._supernetType === 'fast' ? 'fast' : '');
70
- let params;
71
- // In Node.js, ws package is used, so we need to set the auth header
72
- if (isNodejs) {
73
- params = {
74
- headers: {
75
- Authorization: await this.auth.getToken(),
76
- 'User-Agent': userAgent
77
- }
78
- };
79
- }
70
+ const params = await this.auth.socketOptions();
80
71
  this.socket = new WebSocket(url.toString(), params);
81
72
  this.socket.onerror = this.handleError.bind(this);
82
73
  this.socket.onmessage = this.handleMessage.bind(this);
@@ -7,7 +7,8 @@ import { isNotRecoverable } from './WebSocketClient/ErrorCode';
7
7
  import { JSONValue } from '../types/json';
8
8
  import { SupernetType } from './WebSocketClient/types';
9
9
  import { Logger } from '../lib/DefaultLogger';
10
- import AuthManager, { Tokens } from '../lib/AuthManager';
10
+ import CookieAuthManager from '../lib/AuthManager/CookieAuthManager';
11
+ import { AuthManager, TokenAuthManager } from '../lib/AuthManager';
11
12
 
12
13
  const WS_RECONNECT_ATTEMPTS = 5;
13
14
 
@@ -33,6 +34,16 @@ export class ApiError extends Error {
33
34
  }
34
35
  }
35
36
 
37
+ export interface ApiClientOptions {
38
+ baseUrl: string;
39
+ socketUrl: string;
40
+ appId: string;
41
+ networkType: SupernetType;
42
+ logger: Logger;
43
+ authType: 'token' | 'cookies';
44
+ disableSocket?: boolean;
45
+ }
46
+
36
47
  class ApiClient extends TypedEventEmitter<ApiClientEvents> {
37
48
  readonly appId: string;
38
49
  readonly logger: Logger;
@@ -42,23 +53,24 @@ class ApiClient extends TypedEventEmitter<ApiClientEvents> {
42
53
  private _reconnectAttempts = WS_RECONNECT_ATTEMPTS;
43
54
  private _disableSocket: boolean = false;
44
55
 
45
- constructor(
46
- baseUrl: string,
47
- socketUrl: string,
48
- appId: string,
49
- networkType: SupernetType,
50
- logger: Logger,
51
- disableSocket: boolean = false
52
- ) {
56
+ constructor({
57
+ baseUrl,
58
+ socketUrl,
59
+ appId,
60
+ networkType,
61
+ authType,
62
+ logger,
63
+ disableSocket = false
64
+ }: ApiClientOptions) {
53
65
  super();
54
66
  this.appId = appId;
55
67
  this.logger = logger;
56
- this._auth = new AuthManager(baseUrl, logger);
68
+ this._auth =
69
+ authType === 'token' ? new TokenAuthManager(baseUrl, logger) : new CookieAuthManager(logger);
57
70
  this._rest = new RestClient(baseUrl, this._auth, logger);
58
71
  this._socket = new WebSocketClient(socketUrl, this._auth, appId, networkType, logger);
59
72
  this._disableSocket = disableSocket;
60
-
61
- this._auth.on('refreshFailed', this.handleRefreshFailed.bind(this));
73
+ this._auth.on('updated', this.handleAuthUpdated.bind(this));
62
74
  this._socket.on('connected', this.handleSocketConnect.bind(this));
63
75
  this._socket.on('disconnected', this.handleSocketDisconnect.bind(this));
64
76
  }
@@ -67,7 +79,7 @@ class ApiClient extends TypedEventEmitter<ApiClientEvents> {
67
79
  return this.auth.isAuthenticated;
68
80
  }
69
81
 
70
- get auth(): AuthManager {
82
+ get auth() {
71
83
  return this._auth;
72
84
  }
73
85
 
@@ -83,20 +95,6 @@ class ApiClient extends TypedEventEmitter<ApiClientEvents> {
83
95
  return !this._disableSocket;
84
96
  }
85
97
 
86
- async authenticate(tokens: Tokens) {
87
- await this.auth.setTokens(tokens);
88
- if (!this._disableSocket) {
89
- await this.socket.connect();
90
- }
91
- }
92
-
93
- removeAuth() {
94
- this.auth.clear();
95
- if (this.socket.isConnected) {
96
- this.socket.disconnect();
97
- }
98
- }
99
-
100
98
  handleSocketConnect({ network }: ServerConnectData) {
101
99
  this._reconnectAttempts = WS_RECONNECT_ATTEMPTS;
102
100
  this.emit('connected', { network });
@@ -104,13 +102,13 @@ class ApiClient extends TypedEventEmitter<ApiClientEvents> {
104
102
 
105
103
  handleSocketDisconnect(data: ServerDisconnectData) {
106
104
  if (!data.code || isNotRecoverable(data.code)) {
107
- this.removeAuth();
105
+ this.auth.clear();
108
106
  this.emit('disconnected', data);
109
107
  this.logger.error('Not recoverable socket error', data);
110
108
  return;
111
109
  }
112
110
  if (this._reconnectAttempts <= 0) {
113
- this.removeAuth();
111
+ this.auth.clear();
114
112
  this.emit('disconnected', data);
115
113
  this._reconnectAttempts = WS_RECONNECT_ATTEMPTS;
116
114
  return;
@@ -119,8 +117,14 @@ class ApiClient extends TypedEventEmitter<ApiClientEvents> {
119
117
  setTimeout(() => this.socket.connect(), 1000);
120
118
  }
121
119
 
122
- handleRefreshFailed() {
123
- this.removeAuth();
120
+ handleAuthUpdated(isAuthenticated: boolean) {
121
+ if (!isAuthenticated) {
122
+ if (this.socket.isConnected) {
123
+ this.socket.disconnect();
124
+ }
125
+ } else if (!this._disableSocket) {
126
+ this.socket.connect();
127
+ }
124
128
  }
125
129
  }
126
130
 
package/src/index.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  import AccountApi from './Account';
3
3
  import CurrentAccount from './Account/CurrentAccount';
4
4
  // ApiClient
5
- import ApiClient, { ApiError } from './ApiClient';
5
+ import ApiClient, { ApiError, ApiResponse } from './ApiClient';
6
6
  import { SupernetType } from './ApiClient/WebSocketClient/types';
7
7
  import { ApiConfig } from './ApiGroup';
8
8
  // Utils
@@ -12,12 +12,20 @@ import EIP712Helper from './lib/EIP712Helper';
12
12
  import ProjectsApi from './Projects';
13
13
  import Job, { JobStatus } from './Projects/Job';
14
14
  import Project, { ProjectStatus } from './Projects/Project';
15
- import { AvailableModel, OutputFormat, ProjectParams, Scheduler, TimeStepSpacing } from './Projects/types';
15
+ import {
16
+ AvailableModel,
17
+ OutputFormat,
18
+ ProjectParams,
19
+ Scheduler,
20
+ TimeStepSpacing
21
+ } from './Projects/types';
16
22
  // Stats API
17
23
  import StatsApi from './Stats';
18
24
  // Base Types
19
25
  import ErrorData from './types/ErrorData';
20
26
  import { TokenType } from './types/token';
27
+ import { CookieAuthManager, TokenAuthData, TokenAuthManager } from './lib/AuthManager';
28
+ import { MeData } from './Account/types';
21
29
 
22
30
  export type {
23
31
  AvailableModel,
@@ -75,6 +83,16 @@ export interface SogniClientConfig {
75
83
  * If true, the client will connect to the testnet. Ignored if jsonRpcUrl is provided
76
84
  */
77
85
  testnet?: boolean;
86
+ /**
87
+ * Authentication type to use. Can be 'token' or 'cookie'. If not provided, 'token' will be used.
88
+ * `token` authentication relies on a token stored in the client instance. This is what 3rd party
89
+ * Node.js apps should use.
90
+ * `cookie` authentication relies on htmlOnly cookie, set by the server. This will only work for
91
+ * browser apps located on .sogni.ai subdomains due to CORS restrictions.
92
+ * @default 'token'
93
+ * @experimental
94
+ */
95
+ authType?: 'token' | 'cookies';
78
96
  }
79
97
 
80
98
  export class SogniClient {
@@ -96,6 +114,41 @@ export class SogniClient {
96
114
  return this.account.currentAccount;
97
115
  }
98
116
 
117
+ /**
118
+ * When using token authentication, this method can be used to set the tokens.
119
+ * This is useful when the tokens are stored in a secure location and you want to resume the session.
120
+ * @param tokens
121
+ */
122
+ async setTokens(tokens: TokenAuthData): Promise<void> {
123
+ const auth = this.apiClient.auth;
124
+ if (!(auth instanceof TokenAuthManager)) {
125
+ throw new Error('setTokens can only be used with token authentication');
126
+ }
127
+ await auth.authenticate(tokens);
128
+ await this.account.me();
129
+ }
130
+
131
+ /**
132
+ * When using cookie authentication, client has no way to detect if the user is authenticated or not.
133
+ * This method can be used to check if the user is authenticated and populate the currentAccount.
134
+ * @returns
135
+ */
136
+ async checkAuth(): Promise<boolean> {
137
+ const auth = this.apiClient.auth;
138
+ if (!(auth instanceof CookieAuthManager)) {
139
+ throw Error('This method should only be called when using cookie auth');
140
+ }
141
+ try {
142
+ await this.apiClient.rest.get<ApiResponse<MeData>>('/v1/account/me');
143
+ await auth.authenticate();
144
+ await this.account.me();
145
+ return true;
146
+ } catch (e) {
147
+ this.apiClient.logger.info('Client is not authenticated');
148
+ return false;
149
+ }
150
+ }
151
+
99
152
  /**
100
153
  * Create client instance, with default configuration
101
154
  * @param config
@@ -107,14 +160,15 @@ export class SogniClient {
107
160
  const logger = config.logger || new DefaultLogger(config.logLevel || 'warn');
108
161
  const isTestnet = config.testnet !== undefined ? config.testnet : false;
109
162
 
110
- const client = new ApiClient(
111
- restEndpoint,
112
- socketEndpoint,
113
- config.appId,
114
- network,
163
+ const client = new ApiClient({
164
+ baseUrl: restEndpoint,
165
+ socketUrl: socketEndpoint,
166
+ appId: config.appId,
167
+ networkType: network,
115
168
  logger,
116
- config.disableSocket
117
- );
169
+ authType: config.authType || 'token',
170
+ disableSocket: config.disableSocket
171
+ });
118
172
  const eip712 = new EIP712Helper({
119
173
  name: isTestnet ? 'Sogni-testnet' : 'Sogni AI',
120
174
  version: '1',
@@ -0,0 +1,37 @@
1
+ import { Logger } from '../DefaultLogger';
2
+ import TypedEventEmitter from '../TypedEventEmitter';
3
+ import { ClientOptions } from 'ws';
4
+
5
+ interface AuthManagerEvents {
6
+ updated: boolean;
7
+ }
8
+
9
+ abstract class AuthManagerBase<AuthData = never> extends TypedEventEmitter<AuthManagerEvents> {
10
+ protected _logger: Logger;
11
+ constructor(logger: Logger) {
12
+ super();
13
+ this._logger = logger;
14
+ }
15
+
16
+ abstract get isAuthenticated(): boolean;
17
+
18
+ abstract authenticateRequest(option: RequestInit): Promise<RequestInit>;
19
+
20
+ abstract socketOptions(): Promise<ClientOptions | undefined>;
21
+
22
+ /**
23
+ * Get the current authentication data to persist it
24
+ * @returns
25
+ */
26
+ abstract backup(): Promise<AuthData>;
27
+
28
+ /**
29
+ * Restore authentication from the data that was previously backed up
30
+ * @param data
31
+ */
32
+ abstract authenticate(data: AuthData): Promise<void>;
33
+
34
+ abstract clear(): void;
35
+ }
36
+
37
+ export default AuthManagerBase;
@@ -0,0 +1,40 @@
1
+ import AuthManagerBase from './AuthManagerBase';
2
+
3
+ class CookieAuthManager extends AuthManagerBase<undefined> {
4
+ private _isAuthenticated = false;
5
+
6
+ /**
7
+ * For cookie authentication, there is no way to detect if the user is authenticated or not,
8
+ * except when a call to API was successful. So the REST client will set this property to true.
9
+ */
10
+ async authenticate(): Promise<void> {
11
+ this._isAuthenticated = true;
12
+ this.emit('updated', true);
13
+ }
14
+
15
+ clear() {
16
+ this._isAuthenticated = false;
17
+ this.emit('updated', false);
18
+ }
19
+
20
+ get isAuthenticated() {
21
+ return this._isAuthenticated;
22
+ }
23
+
24
+ backup(): Promise<undefined> {
25
+ throw new Error('Not supported with cookie authentication');
26
+ }
27
+
28
+ async authenticateRequest(options: RequestInit): Promise<RequestInit> {
29
+ return {
30
+ ...options,
31
+ credentials: 'include'
32
+ };
33
+ }
34
+
35
+ async socketOptions(): Promise<undefined> {
36
+ return undefined;
37
+ }
38
+ }
39
+
40
+ export default CookieAuthManager;