@emilgroup/account-sdk 1.49.1-beta.20 → 1.49.1-beta.21

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 CHANGED
@@ -17,11 +17,11 @@ Although this package can be used in both TypeScript and JavaScript, it is inten
17
17
  Navigate to the folder of your consuming project and run one of the following commands:
18
18
 
19
19
  ```
20
- npm install @emilgroup/account-sdk@1.49.1-beta.20 --save
20
+ npm install @emilgroup/account-sdk@1.49.1-beta.21 --save
21
21
  ```
22
22
  or
23
23
  ```
24
- yarn add @emilgroup/account-sdk@1.49.1-beta.20
24
+ yarn add @emilgroup/account-sdk@1.49.1-beta.21
25
25
  ```
26
26
 
27
27
  And then you can import `AccountsApi`.
@@ -67,8 +67,13 @@ export const AccountsApiAxiosParamCreator = function (configuration?: Configurat
67
67
  // http bearer authentication required
68
68
  await setBearerAuthToObject(localVarHeaderParameter, configuration)
69
69
 
70
- if (authorization !== undefined && authorization !== null || baseAccessToken !== undefined && baseAccessToken !== null) {
71
- localVarHeaderParameter['Authorization'] = String(authorization ? authorization : baseAccessToken);
70
+ // Only Authorization falls back to the configured token. Without a session it is sent as before, empty when
71
+ // no token is stored, so a refused call still refreshes; a session call (account header) sends none.
72
+ const authorizationIsAuth = 'Authorization'.toLowerCase() === 'authorization';
73
+ const authorizationValue = authorizationIsAuth ? (authorization || baseAccessToken) : authorization;
74
+ const authorizationKeepEmpty = authorizationIsAuth && authorizationValue === '' && !baseOptions?.headers?.['X-Emil-Account'];
75
+ if (authorizationValue || authorizationKeepEmpty) {
76
+ localVarHeaderParameter['Authorization'] = String(authorizationValue);
72
77
  }
73
78
 
74
79
 
@@ -115,8 +120,13 @@ export const AccountsApiAxiosParamCreator = function (configuration?: Configurat
115
120
  // http bearer authentication required
116
121
  await setBearerAuthToObject(localVarHeaderParameter, configuration)
117
122
 
118
- if (authorization !== undefined && authorization !== null || baseAccessToken !== undefined && baseAccessToken !== null) {
119
- localVarHeaderParameter['Authorization'] = String(authorization ? authorization : baseAccessToken);
123
+ // Only Authorization falls back to the configured token. Without a session it is sent as before, empty when
124
+ // no token is stored, so a refused call still refreshes; a session call (account header) sends none.
125
+ const authorizationIsAuth = 'Authorization'.toLowerCase() === 'authorization';
126
+ const authorizationValue = authorizationIsAuth ? (authorization || baseAccessToken) : authorization;
127
+ const authorizationKeepEmpty = authorizationIsAuth && authorizationValue === '' && !baseOptions?.headers?.['X-Emil-Account'];
128
+ if (authorizationValue || authorizationKeepEmpty) {
129
+ localVarHeaderParameter['Authorization'] = String(authorizationValue);
120
130
  }
121
131
 
122
132
 
@@ -165,8 +175,13 @@ export const AccountsApiAxiosParamCreator = function (configuration?: Configurat
165
175
  localVarQueryParameter['expand'] = expand;
166
176
  }
167
177
 
168
- if (authorization !== undefined && authorization !== null || baseAccessToken !== undefined && baseAccessToken !== null) {
169
- localVarHeaderParameter['Authorization'] = String(authorization ? authorization : baseAccessToken);
178
+ // Only Authorization falls back to the configured token. Without a session it is sent as before, empty when
179
+ // no token is stored, so a refused call still refreshes; a session call (account header) sends none.
180
+ const authorizationIsAuth = 'Authorization'.toLowerCase() === 'authorization';
181
+ const authorizationValue = authorizationIsAuth ? (authorization || baseAccessToken) : authorization;
182
+ const authorizationKeepEmpty = authorizationIsAuth && authorizationValue === '' && !baseOptions?.headers?.['X-Emil-Account'];
183
+ if (authorizationValue || authorizationKeepEmpty) {
184
+ localVarHeaderParameter['Authorization'] = String(authorizationValue);
170
185
  }
171
186
 
172
187
 
@@ -215,8 +230,13 @@ export const AccountsApiAxiosParamCreator = function (configuration?: Configurat
215
230
  localVarQueryParameter['expand'] = expand;
216
231
  }
217
232
 
218
- if (authorization !== undefined && authorization !== null || baseAccessToken !== undefined && baseAccessToken !== null) {
219
- localVarHeaderParameter['Authorization'] = String(authorization ? authorization : baseAccessToken);
233
+ // Only Authorization falls back to the configured token. Without a session it is sent as before, empty when
234
+ // no token is stored, so a refused call still refreshes; a session call (account header) sends none.
235
+ const authorizationIsAuth = 'Authorization'.toLowerCase() === 'authorization';
236
+ const authorizationValue = authorizationIsAuth ? (authorization || baseAccessToken) : authorization;
237
+ const authorizationKeepEmpty = authorizationIsAuth && authorizationValue === '' && !baseOptions?.headers?.['X-Emil-Account'];
238
+ if (authorizationValue || authorizationKeepEmpty) {
239
+ localVarHeaderParameter['Authorization'] = String(authorizationValue);
220
240
  }
221
241
 
222
242
 
@@ -291,8 +311,13 @@ export const AccountsApiAxiosParamCreator = function (configuration?: Configurat
291
311
  localVarQueryParameter['filters'] = filters;
292
312
  }
293
313
 
294
- if (authorization !== undefined && authorization !== null || baseAccessToken !== undefined && baseAccessToken !== null) {
295
- localVarHeaderParameter['Authorization'] = String(authorization ? authorization : baseAccessToken);
314
+ // Only Authorization falls back to the configured token. Without a session it is sent as before, empty when
315
+ // no token is stored, so a refused call still refreshes; a session call (account header) sends none.
316
+ const authorizationIsAuth = 'Authorization'.toLowerCase() === 'authorization';
317
+ const authorizationValue = authorizationIsAuth ? (authorization || baseAccessToken) : authorization;
318
+ const authorizationKeepEmpty = authorizationIsAuth && authorizationValue === '' && !baseOptions?.headers?.['X-Emil-Account'];
319
+ if (authorizationValue || authorizationKeepEmpty) {
320
+ localVarHeaderParameter['Authorization'] = String(authorizationValue);
296
321
  }
297
322
 
298
323
 
@@ -339,8 +364,13 @@ export const AccountsApiAxiosParamCreator = function (configuration?: Configurat
339
364
  // http bearer authentication required
340
365
  await setBearerAuthToObject(localVarHeaderParameter, configuration)
341
366
 
342
- if (authorization !== undefined && authorization !== null || baseAccessToken !== undefined && baseAccessToken !== null) {
343
- localVarHeaderParameter['Authorization'] = String(authorization ? authorization : baseAccessToken);
367
+ // Only Authorization falls back to the configured token. Without a session it is sent as before, empty when
368
+ // no token is stored, so a refused call still refreshes; a session call (account header) sends none.
369
+ const authorizationIsAuth = 'Authorization'.toLowerCase() === 'authorization';
370
+ const authorizationValue = authorizationIsAuth ? (authorization || baseAccessToken) : authorization;
371
+ const authorizationKeepEmpty = authorizationIsAuth && authorizationValue === '' && !baseOptions?.headers?.['X-Emil-Account'];
372
+ if (authorizationValue || authorizationKeepEmpty) {
373
+ localVarHeaderParameter['Authorization'] = String(authorizationValue);
344
374
  }
345
375
 
346
376
 
@@ -741,7 +771,7 @@ export class AccountsApi extends BaseAPI {
741
771
  * @memberof AccountsApi
742
772
  */
743
773
  public createAccount(requestParameters: AccountsApiCreateAccountRequest, options?: AxiosRequestConfig) {
744
- return AccountsApiFp(this.configuration).createAccount(requestParameters.createAccountRequestDto, requestParameters.authorization, options).then((request) => request(this.axios, this.basePath));
774
+ return AccountsApiFp(this.configuration).createAccount(requestParameters.createAccountRequestDto, requestParameters.authorization, this.withOwner(options)).then((request) => request(this.axios, this.basePath));
745
775
  }
746
776
 
747
777
  /**
@@ -753,7 +783,7 @@ export class AccountsApi extends BaseAPI {
753
783
  * @memberof AccountsApi
754
784
  */
755
785
  public deleteAccount(requestParameters: AccountsApiDeleteAccountRequest, options?: AxiosRequestConfig) {
756
- return AccountsApiFp(this.configuration).deleteAccount(requestParameters.code, requestParameters.authorization, options).then((request) => request(this.axios, this.basePath));
786
+ return AccountsApiFp(this.configuration).deleteAccount(requestParameters.code, requestParameters.authorization, this.withOwner(options)).then((request) => request(this.axios, this.basePath));
757
787
  }
758
788
 
759
789
  /**
@@ -765,7 +795,7 @@ export class AccountsApi extends BaseAPI {
765
795
  * @memberof AccountsApi
766
796
  */
767
797
  public getAccount(requestParameters: AccountsApiGetAccountRequest, options?: AxiosRequestConfig) {
768
- return AccountsApiFp(this.configuration).getAccount(requestParameters.code, requestParameters.authorization, requestParameters.expand, options).then((request) => request(this.axios, this.basePath));
798
+ return AccountsApiFp(this.configuration).getAccount(requestParameters.code, requestParameters.authorization, requestParameters.expand, this.withOwner(options)).then((request) => request(this.axios, this.basePath));
769
799
  }
770
800
 
771
801
  /**
@@ -777,7 +807,7 @@ export class AccountsApi extends BaseAPI {
777
807
  * @memberof AccountsApi
778
808
  */
779
809
  public getAccountByEmail(requestParameters: AccountsApiGetAccountByEmailRequest, options?: AxiosRequestConfig) {
780
- return AccountsApiFp(this.configuration).getAccountByEmail(requestParameters.email, requestParameters.authorization, requestParameters.expand, options).then((request) => request(this.axios, this.basePath));
810
+ return AccountsApiFp(this.configuration).getAccountByEmail(requestParameters.email, requestParameters.authorization, requestParameters.expand, this.withOwner(options)).then((request) => request(this.axios, this.basePath));
781
811
  }
782
812
 
783
813
  /**
@@ -789,7 +819,7 @@ export class AccountsApi extends BaseAPI {
789
819
  * @memberof AccountsApi
790
820
  */
791
821
  public listAccounts(requestParameters: AccountsApiListAccountsRequest = {}, options?: AxiosRequestConfig) {
792
- return AccountsApiFp(this.configuration).listAccounts(requestParameters.authorization, requestParameters.pageSize, requestParameters.pageToken, requestParameters.filter, requestParameters.search, requestParameters.order, requestParameters.expand, requestParameters.filters, options).then((request) => request(this.axios, this.basePath));
822
+ return AccountsApiFp(this.configuration).listAccounts(requestParameters.authorization, requestParameters.pageSize, requestParameters.pageToken, requestParameters.filter, requestParameters.search, requestParameters.order, requestParameters.expand, requestParameters.filters, this.withOwner(options)).then((request) => request(this.axios, this.basePath));
793
823
  }
794
824
 
795
825
  /**
@@ -801,6 +831,6 @@ export class AccountsApi extends BaseAPI {
801
831
  * @memberof AccountsApi
802
832
  */
803
833
  public updateAccount(requestParameters: AccountsApiUpdateAccountRequest, options?: AxiosRequestConfig) {
804
- return AccountsApiFp(this.configuration).updateAccount(requestParameters.code, requestParameters.updateAccountRequestDto, requestParameters.authorization, options).then((request) => request(this.axios, this.basePath));
834
+ return AccountsApiFp(this.configuration).updateAccount(requestParameters.code, requestParameters.updateAccountRequestDto, requestParameters.authorization, this.withOwner(options)).then((request) => request(this.axios, this.basePath));
805
835
  }
806
836
  }
package/api/health-api.ts CHANGED
@@ -119,6 +119,6 @@ export class HealthApi extends BaseAPI {
119
119
  * @memberof HealthApi
120
120
  */
121
121
  public check(options?: AxiosRequestConfig) {
122
- return HealthApiFp(this.configuration).check(options).then((request) => request(this.axios, this.basePath));
122
+ return HealthApiFp(this.configuration).check(this.withOwner(options)).then((request) => request(this.axios, this.basePath));
123
123
  }
124
124
  }
package/base.ts CHANGED
@@ -32,9 +32,20 @@ export const COLLECTION_FORMATS = {
32
32
  pipes: "|",
33
33
  };
34
34
 
35
+ export interface SessionClass {
36
+ sub: string;
37
+ username?: string;
38
+ email?: string;
39
+ tenantSlug?: string;
40
+ permissions?: string;
41
+ expiresAt?: number;
42
+ }
43
+
35
44
  export interface LoginClass {
36
45
  accessToken: string;
37
46
  permissions: string;
47
+ authMode?: string;
48
+ session?: SessionClass;
38
49
  }
39
50
 
40
51
  export interface SwitchWorkspaceRequest {
@@ -45,6 +56,8 @@ export interface SwitchWorkspaceRequest {
45
56
  export interface SwitchWorkspaceResponseClass {
46
57
  accessToken: string;
47
58
  permissions: string;
59
+ authMode?: string;
60
+ session?: SessionClass;
48
61
  }
49
62
 
50
63
  export enum Environment {
@@ -59,6 +72,10 @@ export enum Environment {
59
72
  let _retry_count = 0
60
73
  let _retry = null
61
74
 
75
+ // Marks each request with the account (username) of the SDK object that sent it: only SDK objects of that
76
+ // account refresh and resend it, never an object of another account open in the same browser.
77
+ const REQUEST_OWNER = '_emilSdkOwner';
78
+
62
79
  export function resetRetry() {
63
80
  _retry_count = 0
64
81
  }
@@ -77,10 +94,19 @@ interface TokenData {
77
94
  accessToken?: string;
78
95
  username?: string;
79
96
  permissions?: string;
97
+ // 'both' or 'session' when authservice keeps the tokens in a server session.
98
+ authMode?: string;
99
+ sub?: string;
80
100
  }
81
101
 
82
102
  const NETWORK_ERROR_MESSAGE = "Network Error";
83
103
  const TOKEN_DATA = 'APP_TOKEN';
104
+ // Sent only to authservice, to ask for a server session.
105
+ const AUTH_MODE_HEADER = 'X-Emil-Auth-Mode';
106
+ // Names the account on every call; one browser session can hold several.
107
+ const ACCOUNT_HEADER = 'X-Emil-Account';
108
+ const SESSION_MODES = ['both', 'session'];
109
+ const RESEND_SAFE_METHODS = ['get', 'head', 'options'];
84
110
 
85
111
  /**
86
112
  *
@@ -98,14 +124,31 @@ export class BaseAPI {
98
124
  this.loadTokenData();
99
125
 
100
126
  if (configuration) {
127
+ // Two accounts in one browser share APP_TOKEN: never take the token or session account saved for
128
+ // another account. The mode stays (an app naming its account keeps its session). Saved with the next refresh.
129
+ const ownUser = configuration.username;
130
+ const storedUser = this.tokenData.username;
131
+ if (ownUser && storedUser && ownUser.toLowerCase() !== storedUser.toLowerCase()) {
132
+ delete this.tokenData.accessToken;
133
+ delete this.tokenData.sub;
134
+ this.tokenData.username = ownUser;
135
+ }
101
136
  const { accessToken } = this.tokenData;
102
137
  this.configuration = configuration;
103
138
  this.basePath = configuration.basePath || this.basePath;
104
139
 
105
- // Use config token if provided, otherwise use tokenData token
106
- const configToken = this.configuration.accessToken;
107
- const storedToken = accessToken ? `Bearer ${accessToken}` : '';
108
- this.configuration.accessToken = configToken || storedToken;
140
+ if (configuration.cookieAuth) {
141
+ this.configuration.username = this.configuration.username || this.tokenData.username;
142
+ }
143
+
144
+ if (this.usesSession()) {
145
+ this.useSessionAuth();
146
+ } else {
147
+ // Use config token if provided, otherwise use tokenData token
148
+ const configToken = this.configuration.accessToken;
149
+ const storedToken = accessToken ? `Bearer ${accessToken}` : '';
150
+ this.configuration.accessToken = configToken || storedToken;
151
+ }
109
152
  } else {
110
153
  const { accessToken, username } = this.tokenData;
111
154
 
@@ -119,6 +162,110 @@ export class BaseAPI {
119
162
  this.attachInterceptor(axios);
120
163
  }
121
164
 
165
+ // The app's own account wins over APP_TOKEN, which holds whichever tab wrote last.
166
+ private sessionAccount(): string | undefined {
167
+ return this.configuration.account || this.tokenData.sub;
168
+ }
169
+
170
+ // Apps may pass the address as the second argument only; authservice is on the same host.
171
+ private authBasePath(): string {
172
+ return this.configuration.basePath || this.basePath;
173
+ }
174
+
175
+ private usesSession(): boolean {
176
+ return !!this.configuration.cookieAuth && SESSION_MODES.indexOf(this.tokenData.authMode) !== -1 && !!this.sessionAccount();
177
+ }
178
+
179
+ private useSessionAuth() {
180
+ const { headers, ...baseOptions } = this.configuration.baseOptions || {};
181
+ this.configuration.accessToken = '';
182
+ this.configuration.baseOptions = {
183
+ ...baseOptions,
184
+ withCredentials: true,
185
+ headers: { ...headers, [ACCOUNT_HEADER]: this.sessionAccount() },
186
+ };
187
+ }
188
+
189
+ private useHeaderAuth(accessToken: string, keepSessionData = false) {
190
+ this.configuration.accessToken = accessToken ? `Bearer ${accessToken}` : '';
191
+ this.tokenData.accessToken = accessToken;
192
+ // An app without cookieAuth does not own the session fields: its refresh leaves them for the session apps.
193
+ if (!keepSessionData || this.configuration.cookieAuth) {
194
+ delete this.tokenData.authMode;
195
+ delete this.tokenData.sub;
196
+ }
197
+ if (this.configuration.cookieAuth) {
198
+ const { withCredentials, headers, ...baseOptions } = this.configuration.baseOptions || {};
199
+ const { [ACCOUNT_HEADER]: account, ...otherHeaders } = headers || {};
200
+ this.configuration.baseOptions = { ...baseOptions, headers: otherHeaders };
201
+ }
202
+ }
203
+
204
+ private applyTokenReply({ accessToken, authMode, session }: LoginClass, fromRefresh = false) {
205
+ if (this.configuration.cookieAuth && SESSION_MODES.indexOf(authMode) !== -1 && session?.sub) {
206
+ this.tokenData.authMode = authMode;
207
+ this.tokenData.sub = session.sub;
208
+ if (this.configuration.account) {
209
+ // A workspace switch moves the app to another account.
210
+ this.configuration.account = session.sub;
211
+ }
212
+ // Both keeps the token for apps not moved yet; session mode keeps no token at all.
213
+ if (authMode === 'both' && accessToken) {
214
+ this.tokenData.accessToken = accessToken;
215
+ } else {
216
+ delete this.tokenData.accessToken;
217
+ }
218
+ this.useSessionAuth();
219
+ } else {
220
+ this.useHeaderAuth(accessToken, fromRefresh);
221
+ }
222
+ }
223
+
224
+ private sessionHeaders(): Record<string, string> {
225
+ if (!this.configuration.cookieAuth) {
226
+ return {};
227
+ }
228
+ return {
229
+ [AUTH_MODE_HEADER]: 'session',
230
+ ...(this.usesSession() && { [ACCOUNT_HEADER]: this.sessionAccount() }),
231
+ };
232
+ }
233
+
234
+ private withCurrentAuth(config: AxiosRequestConfig): AxiosRequestConfig {
235
+ delete config.headers['Authorization'];
236
+ delete config.headers[ACCOUNT_HEADER];
237
+ if (this.usesSession()) {
238
+ config.headers[ACCOUNT_HEADER] = this.sessionAccount();
239
+ config.withCredentials = true;
240
+ } else if (this.configuration.accessToken) {
241
+ config.headers['Authorization'] = this.configuration.accessToken;
242
+ }
243
+ return config;
244
+ }
245
+
246
+ // Both keeps the token, so a session call the browser could not complete can be sent again with it.
247
+ private canRetryWithToken(config: AxiosRequestConfig): boolean {
248
+ return !!config.withCredentials && !!config.headers?.[ACCOUNT_HEADER]
249
+ && !!this.tokenData.accessToken && this.tokenData.authMode !== 'session';
250
+ }
251
+
252
+ // A network error can come after the server ran the call, so only a call that is safe to repeat is sent again.
253
+ private canResend(config: AxiosRequestConfig): boolean {
254
+ return RESEND_SAFE_METHODS.indexOf((config.method || 'get').toLowerCase()) !== -1
255
+ || Object.keys(config.headers || {}).some((name) => name.toLowerCase() === 'idempotency-key');
256
+ }
257
+
258
+ private switchToToken() {
259
+ this.useHeaderAuth(this.tokenData.accessToken);
260
+ this.storeTokenData(this.tokenData);
261
+ }
262
+
263
+ private retryWithToken(axios: AxiosInstance, config: AxiosRequestConfig) {
264
+ this.switchToToken();
265
+ config.withCredentials = false;
266
+ return axios(this.withCurrentAuth(config));
267
+ }
268
+
122
269
  selectEnvironment(env: Environment) {
123
270
  this.selectBasePath(env);
124
271
  }
@@ -138,8 +285,8 @@ export class BaseAPI {
138
285
  async authorize(username: string, password: string, targetWorkspace?: string): Promise<void> {
139
286
  const options: AxiosRequestConfig = {
140
287
  method: 'POST',
141
- url: `${this.configuration.basePath}/authservice/v1/login`,
142
- headers: { 'Content-Type': 'application/json' },
288
+ url: `${this.authBasePath()}/authservice/v1/login`,
289
+ headers: { 'Content-Type': 'application/json', ...(this.configuration.cookieAuth && { [AUTH_MODE_HEADER]: 'session' }) },
143
290
  data: {
144
291
  username,
145
292
  password,
@@ -149,13 +296,10 @@ export class BaseAPI {
149
296
 
150
297
  const response = await globalAxios.request<LoginClass>(options);
151
298
 
152
- const { data: { accessToken, permissions } } = response;
153
-
154
299
  this.configuration.username = username;
155
- this.configuration.accessToken = `Bearer ${accessToken}`;
156
300
  this.tokenData.username = username;
157
- this.tokenData.accessToken = accessToken;
158
- this.tokenData.permissions = permissions;
301
+ this.tokenData.permissions = response.data.permissions;
302
+ this.applyTokenReply(response.data);
159
303
 
160
304
  // Switch workspace if provided
161
305
  if (targetWorkspace) {
@@ -175,10 +319,11 @@ export class BaseAPI {
175
319
  async switchWorkspace(targetWorkspace: string): Promise<void> {
176
320
  const options: AxiosRequestConfig = {
177
321
  method: 'POST',
178
- url: `${this.configuration.basePath}/authservice/v1/workspaces/switch`,
322
+ url: `${this.authBasePath()}/authservice/v1/workspaces/switch`,
179
323
  headers: {
180
324
  'Content-Type': 'application/json',
181
- 'Authorization': `Bearer ${this.configuration.accessToken}`,
325
+ ...this.sessionHeaders(),
326
+ ...(!this.usesSession() && !!this.configuration.accessToken && { 'Authorization': this.configuration.accessToken as string }),
182
327
  },
183
328
  data: {
184
329
  username: this.configuration.username,
@@ -189,11 +334,8 @@ export class BaseAPI {
189
334
 
190
335
  const response = await globalAxios.request<SwitchWorkspaceResponseClass>(options);
191
336
 
192
- const { data: { accessToken, permissions } } = response;
193
-
194
- this.configuration.accessToken = `Bearer ${accessToken}`;
195
- this.tokenData.accessToken = accessToken;
196
- this.tokenData.permissions = permissions;
337
+ this.tokenData.permissions = response.data.permissions;
338
+ this.applyTokenReply(response.data);
197
339
 
198
340
  this.storeTokenData({
199
341
  ...this.tokenData
@@ -209,9 +351,34 @@ export class BaseAPI {
209
351
 
210
352
  const refreshTokenAxios = globalAxios.create()
211
353
  refreshTokenAxios.interceptors.response.use(response => {
212
- const { permissions } = response.data;
213
-
354
+ const { permissions, accessToken, authMode } = response.data;
355
+ // Decided before the merge below: newer session data must not keep the new token from this object.
356
+ const onSession = this.usesSession();
357
+
358
+ // Newer data saved for this account (e.g. the session mode the shell saved later) wins over the copy
359
+ // read when the object was made; data of another account or another workspace (sub) is never taken.
360
+ const latest = ((typeof window !== 'undefined' && defaultStorage().get<TokenData>(TOKEN_DATA)) || {}) as TokenData;
361
+ const ownSub = this.sessionAccount();
362
+ if (latest.username?.toLowerCase() === username.toLowerCase()
363
+ && (!ownSub || !latest.sub || latest.sub === ownSub)) {
364
+ // A bare reset ({username}, an app's account check) keeps this copy's other fields; any fuller record
365
+ // is the truth, so fields it lacks were removed on purpose (e.g. a rollback) and stay removed.
366
+ const bareReset = Object.keys(latest).every((key) => key === 'username');
367
+ this.tokenData = bareReset ? { ...this.tokenData, ...latest } : { ...latest };
368
+ }
369
+ // The reply is for this account: label it, or an SDK object of another account would take the token.
370
+ if (this.tokenData.username?.toLowerCase() !== username.toLowerCase()) {
371
+ this.tokenData.username = username;
372
+ }
214
373
  this.tokenData.permissions = permissions;
374
+ // Keep the new token for the page's next SDK objects; session data is left as it is.
375
+ if (accessToken && !onSession) {
376
+ this.tokenData.accessToken = accessToken;
377
+ this.configuration.accessToken = `Bearer ${accessToken}`;
378
+ } else if (accessToken && authMode === 'both') {
379
+ // Both: this app stays on the session; the token is kept for apps not moved yet.
380
+ this.tokenData.accessToken = accessToken;
381
+ }
215
382
  this.storeTokenData(this.tokenData);
216
383
 
217
384
  return response;
@@ -219,9 +386,10 @@ export class BaseAPI {
219
386
 
220
387
  const options: AxiosRequestConfig = {
221
388
  method: 'POST',
222
- url: `${this.configuration.basePath}/authservice/v1/refresh-token`,
389
+ url: `${this.authBasePath()}/authservice/v1/refresh-token`,
223
390
  headers: {
224
391
  'Content-Type': 'application/json',
392
+ ...this.sessionHeaders(),
225
393
  },
226
394
  data: { username: username },
227
395
  withCredentials: true,
@@ -249,6 +417,18 @@ export class BaseAPI {
249
417
  this.storeTokenData(null);
250
418
  }
251
419
 
420
+ private ownAccount(): string {
421
+ return (this.configuration.username || '').toLowerCase();
422
+ }
423
+
424
+ protected withOwner(options?: AxiosRequestConfig): AxiosRequestConfig {
425
+ // An object without a username cannot refresh itself: its requests stay unmarked, as before.
426
+ if (!this.ownAccount()) {
427
+ return { ...options };
428
+ }
429
+ return { ...options, [REQUEST_OWNER]: this.ownAccount() } as AxiosRequestConfig;
430
+ }
431
+
252
432
  private attachInterceptor(axios: AxiosInstance) {
253
433
  axios.interceptors.response.use(
254
434
  (res) => {
@@ -256,26 +436,33 @@ export class BaseAPI {
256
436
  },
257
437
  async (err) => {
258
438
  let originalConfig = err.config;
439
+ // Sent for another account: only that account's SDK objects handle it. Unmarked requests as before.
440
+ const owner = originalConfig?.[REQUEST_OWNER];
441
+ if (owner !== undefined && owner !== this.ownAccount()) {
442
+ return Promise.reject(err);
443
+ }
259
444
  if (err.response && !(err.response instanceof XMLHttpRequest)) { // sometimes buggy and is of type request
260
445
  // Access Token was expired
261
446
  if ((err.response.status === 401 || err.response.status === 403)
262
447
  && !originalConfig._retry) {
263
448
  originalConfig._retry = true;
264
449
  try {
265
- const { accessToken: tokenString, permissions } = await this.refreshTokenInternal();
266
- const accessToken = `Bearer ${tokenString}`;
267
- this.tokenData.permissions = permissions;
268
-
269
- delete originalConfig.headers['Authorization']
270
-
271
- originalConfig.headers['Authorization'] = accessToken;
272
-
273
- this.configuration.accessToken = accessToken;
274
- this.tokenData.accessToken = tokenString;
275
-
450
+ const reply = await this.refreshTokenInternal();
451
+ this.tokenData.permissions = reply.permissions;
452
+ this.applyTokenReply(reply, true);
276
453
  this.storeTokenData(this.tokenData);
277
454
 
278
- return axios(originalConfig);
455
+ if (!this.usesSession()) {
456
+ return axios(this.withCurrentAuth(originalConfig));
457
+ }
458
+
459
+ // Both mode: if the browser does not send the session cookie (another site), use the token.
460
+ return axios(this.withCurrentAuth(originalConfig)).catch((retryError) => {
461
+ if (retryError?.response?.status !== 401 || !this.tokenData.accessToken) {
462
+ return Promise.reject(retryError);
463
+ }
464
+ return this.retryWithToken(axios, originalConfig);
465
+ });
279
466
  } catch (_error) {
280
467
  if (_error.response && _error.response.data) {
281
468
  return Promise.reject(_error.response.data);
@@ -283,26 +470,35 @@ export class BaseAPI {
283
470
  return Promise.reject(_error);
284
471
  }
285
472
  }
473
+ } else if (err.message === NETWORK_ERROR_MESSAGE && this.canRetryWithToken(originalConfig)) {
474
+ // Both mode: the browser blocked the session call or hid its reply; later calls use the token.
475
+ if (!this.canResend(originalConfig)) {
476
+ this.switchToToken();
477
+ return Promise.reject(err);
478
+ }
479
+ return this.retryWithToken(axios, originalConfig);
286
480
  } else if (err.message === NETWORK_ERROR_MESSAGE
287
- && originalConfig.headers.hasOwnProperty('Authorization')
481
+ && (originalConfig.headers.hasOwnProperty('Authorization')
482
+ || (this.configuration.cookieAuth && originalConfig.withCredentials && this.canResend(originalConfig)))
288
483
  && _retry_count < 4
289
484
  ) {
290
485
  _retry_count++;
291
486
  try {
292
- const { accessToken: tokenString, permissions } = await this.refreshTokenInternal();
293
- const accessToken = `Bearer ${tokenString}`;
294
- this.tokenData.permissions = permissions;
487
+ // In session mode an unreadable 401 (no CORS headers on the error reply) also lands here.
488
+ const reply = await this.refreshTokenInternal();
489
+ this.tokenData.permissions = reply.permissions;
295
490
 
296
491
  _retry = true;
297
- originalConfig.headers['Authorization'] = accessToken;
298
-
299
- this.configuration.accessToken = accessToken;
300
- this.tokenData.accessToken = tokenString;
301
-
492
+ this.applyTokenReply(reply, true);
302
493
  this.storeTokenData(this.tokenData);
303
494
 
495
+ // The limit is for refreshes that do not help; one that worked starts the count again.
496
+ // Not awaited, so a failed retry reaches the app as the same axios error as before.
304
497
  return axios.request({
305
- ...originalConfig,
498
+ ...this.withCurrentAuth(originalConfig),
499
+ }).then((retried) => {
500
+ _retry_count = 0;
501
+ return retried;
306
502
  });
307
503
  } catch (_error) {
308
504
  if (_error.response && _error.response.data) {
package/common.ts CHANGED
@@ -130,9 +130,10 @@ export const toPathString = function (url: URL) {
130
130
  * @export
131
131
  */
132
132
  export const createRequestFunction = function (axiosArgs: RequestArgs, globalAxios: AxiosInstance, BASE_PATH: string, configuration?: Configuration) {
133
- return <T = unknown, R = AxiosResponse<T>>(axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {
133
+ // Typed by hand: since axios 1.20, request() returns an internal axios type that SDK declarations cannot name.
134
+ return <T = unknown, R = AxiosResponse<T>>(axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH): Promise<R> => {
134
135
  const axiosRequestArgs = {...axiosArgs.options, url: (configuration?.basePath || basePath) + axiosArgs.url};
135
- return axios.request<T, R>(axiosRequestArgs);
136
+ return axios.request<T, R>(axiosRequestArgs) as Promise<R>;
136
137
  };
137
138
  }
138
139
 
package/configuration.ts CHANGED
@@ -21,6 +21,8 @@ export interface ConfigurationParameters {
21
21
  basePath?: string;
22
22
  baseOptions?: any;
23
23
  formDataCtor?: new () => any;
24
+ cookieAuth?: boolean;
25
+ account?: string;
24
26
  }
25
27
 
26
28
  export class Configuration {
@@ -74,6 +76,22 @@ export class Configuration {
74
76
  */
75
77
  formDataCtor?: new () => any;
76
78
 
79
+ /**
80
+ * Use a server session when authservice makes one; otherwise the token, as without this option.
81
+ * Set `account` too, so each SDK object keeps its own account.
82
+ *
83
+ * @type {boolean}
84
+ * @memberof Configuration
85
+ */
86
+ cookieAuth?: boolean;
87
+ /**
88
+ * The session account (Cognito sub) this app shows, so the SDK never takes another tab's account.
89
+ *
90
+ * @type {string}
91
+ * @memberof Configuration
92
+ */
93
+ account?: string;
94
+
77
95
  constructor(param: ConfigurationParameters = {}) {
78
96
  this.apiKey = param.apiKey;
79
97
  this.username = param.username;
@@ -82,6 +100,8 @@ export class Configuration {
82
100
  this.basePath = param.basePath;
83
101
  this.baseOptions = param.baseOptions;
84
102
  this.formDataCtor = param.formDataCtor;
103
+ this.cookieAuth = param.cookieAuth;
104
+ this.account = param.account;
85
105
  }
86
106
 
87
107
  /**