oneentry 1.0.151 → 1.0.152

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/changelog.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # SDK Change Log
2
2
 
3
+ ## v.1.0.152
4
+
5
+ ### Bug Fixes
6
+
7
+ - Auth — eliminated the spurious `401` logged on every page load for a valid restored session. `browserResponse` now does a **proactive refresh** when a refresh token is present but there is no access token yet: it fetches the access token *before* the first request (clean `200`) instead of sending an unauthenticated request that is guaranteed to `401` and then reactively refreshing. After the proactive refresh it attaches `Authorization` and drops the `x-guest-id` scoping. The reactive `401 → refresh → retry` path is retained for mid-session token expiry.
8
+
9
+ - Auth — `refreshToken` is now **single-flight**.
10
+
3
11
  ## v.1.0.151
4
12
 
5
13
  ### What's New
@@ -57,11 +57,17 @@ export default abstract class AsyncModules extends SyncModules {
57
57
  */
58
58
  protected _fetchDelete<T = unknown>(path: string, body?: unknown): Promise<T>;
59
59
  /**
60
- * Refreshes the authentication token.
60
+ * Refreshes the authentication token (single-flight).
61
61
  * @returns {Promise<boolean>} A promise resolving to a boolean indicating success or failure.
62
- * @description Define an asynchronous method 'refreshToken' that attempts to refresh the authentication token
62
+ * @description De-duplicates concurrent refreshes the refresh token is single-use, so parallel requests must share one refresh call instead of each burning a rotated token.
63
63
  */
64
64
  protected refreshToken(): Promise<boolean>;
65
+ /**
66
+ * Performs the actual token refresh request (without de-duplication).
67
+ * @returns {Promise<boolean>} A promise resolving to a boolean indicating success or failure.
68
+ * @description Sends POST /users/refresh and, on success, stores the rotated access/refresh tokens and calls saveFunction.
69
+ */
70
+ private _performRefresh;
65
71
  /**
66
72
  * Creates options for HTTP requests.
67
73
  * @param {string} method - The HTTP method (GET, POST, PUT, DELETE, etc.).
@@ -282,11 +282,33 @@ class AsyncModules extends syncModules_1.default {
282
282
  }
283
283
  }
284
284
  /**
285
- * Refreshes the authentication token.
285
+ * Refreshes the authentication token (single-flight).
286
286
  * @returns {Promise<boolean>} A promise resolving to a boolean indicating success or failure.
287
- * @description Define an asynchronous method 'refreshToken' that attempts to refresh the authentication token
287
+ * @description De-duplicates concurrent refreshes the refresh token is single-use, so parallel requests must share one refresh call instead of each burning a rotated token.
288
288
  */
289
289
  async refreshToken() {
290
+ // Reuse an in-flight refresh if one is already running (shared via state
291
+ // across all module instances). Prevents parallel requests from each firing
292
+ // their own refresh and invalidating the single-use refresh token.
293
+ if (this.state._refreshPromise) {
294
+ return this.state._refreshPromise;
295
+ }
296
+ const promise = this._performRefresh();
297
+ this.state._refreshPromise = promise;
298
+ try {
299
+ return await promise;
300
+ }
301
+ finally {
302
+ // Clear so the next genuine 401 (e.g. mid-session expiry) can refresh again.
303
+ this.state._refreshPromise = null;
304
+ }
305
+ }
306
+ /**
307
+ * Performs the actual token refresh request (without de-duplication).
308
+ * @returns {Promise<boolean>} A promise resolving to a boolean indicating success or failure.
309
+ * @description Sends POST /users/refresh and, on success, stores the rotated access/refresh tokens and calls saveFunction.
310
+ */
311
+ async _performRefresh() {
290
312
  const url = this.state.url +
291
313
  `/api/content/users-auth-providers/marker/` +
292
314
  this.state.providerMarker +
@@ -396,7 +418,32 @@ class AsyncModules extends syncModules_1.default {
396
418
  * @description Define an asynchronous method 'browserResponse' that takes a path and options as parameters
397
419
  */
398
420
  async browserResponse(path, options) {
421
+ // Set when a proactive refresh ran and failed (dead/expired refresh token):
422
+ // used below to skip the reactive refresh so we don't fire a second,
423
+ // equally-doomed refresh on the 401 that the request is about to return.
424
+ let proactiveRefreshFailed = false;
399
425
  try {
426
+ // Proactive (eager) refresh: a session restored from storage has a refresh
427
+ // token but no access token yet. Sending the first authenticated request
428
+ // without an access token GUARANTEES a 401 (then a reactive refresh + retry)
429
+ // — a spurious 401 logged on every page load even for a perfectly valid
430
+ // session. Obtaining the access token up-front turns that into a clean 200.
431
+ // Fires at most once per session (skipped once accessToken is set), and is
432
+ // off for custom-auth callers who manage tokens themselves.
433
+ if (!this.state.accessToken &&
434
+ this.state.refreshToken &&
435
+ !this.state.customAuth) {
436
+ const refreshed = await this.refreshToken();
437
+ if (refreshed) {
438
+ // Authenticated now — attach the bearer and drop the guest scoping
439
+ // that makeOptions added while no access token was present.
440
+ options.headers['Authorization'] = 'Bearer ' + this.state.accessToken;
441
+ delete options.headers['x-guest-id'];
442
+ }
443
+ else {
444
+ proactiveRefreshFailed = true;
445
+ }
446
+ }
400
447
  // Perform a fetch request using the full URL obtained from '_getFullPath' and the provided options
401
448
  const response = await fetch(this._getFullPath(path), options);
402
449
  // Check if the response status is OK (status code 200-299)
@@ -415,8 +462,12 @@ class AsyncModules extends syncModules_1.default {
415
462
  }
416
463
  else {
417
464
  // Handle non-OK responses
418
- // Check if the status is 401 (Unauthorized) and custom authentication is not used
419
- if (response.status === 401 && !this.state.customAuth) {
465
+ // Reactive refresh for mid-session expiry: access token was present but
466
+ // the server rejected it. Skipped when a proactive refresh already ran
467
+ // and failed (the refresh token is dead — retrying would 400 again).
468
+ if (response.status === 401 &&
469
+ !this.state.customAuth &&
470
+ !proactiveRefreshFailed) {
420
471
  // Attempt to refresh the access token
421
472
  const refresh = await this.refreshToken();
422
473
  if (refresh) {
@@ -11,6 +11,7 @@ export default class StateModule {
11
11
  accessToken: string | undefined;
12
12
  traficLimit: boolean;
13
13
  refreshToken: string | undefined;
14
+ _refreshPromise: Promise<boolean> | null;
14
15
  providerMarker: string;
15
16
  customAuth: boolean;
16
17
  _NO_FETCH: boolean;
@@ -46,6 +46,10 @@ class StateModule {
46
46
  */
47
47
  constructor(url, config) {
48
48
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1;
49
+ // In-flight token refresh, shared across all module instances (they share this
50
+ // state object). De-duplicates concurrent refreshes so the single-use refresh
51
+ // token is not burned by parallel requests. Null when no refresh is running.
52
+ this._refreshPromise = null;
49
53
  this.url = url;
50
54
  this.lang = (_a = config.langCode) !== null && _a !== void 0 ? _a : 'en_US';
51
55
  this.token = config.token;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oneentry",
3
- "version": "1.0.151",
3
+ "version": "1.0.152",
4
4
  "description": "OneEntry NPM package",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",