@gandalan/weblibs 2.0.7 → 2.0.8

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/api/fluentApi.js CHANGED
@@ -1,4 +1,4 @@
1
- import { restClient } from "./fluentRestClient";
1
+ import { restClient, RestError } from "./fluentRestClient";
2
2
 
3
3
  /**
4
4
  * @typedef {Object} FluentApi
@@ -16,6 +16,7 @@ import { restClient } from "./fluentRestClient";
16
16
  * @property {(url?: string, payload?: Object|FormData|Array<any>|string[]|string|null, auth?: boolean) => Promise<object|Array<any>>} delete - Async function to perform DELETE requests.
17
17
  * @property {() => import("./fluentRestClient").FluentRESTClient} createRestClient - Creates a configured REST client.
18
18
  * @property {(auth?: boolean) => Promise<void>} preCheck - Ensures authentication before a request when required.
19
+ * @property {(auth: boolean, executeRequest: () => Promise<any>) => Promise<any>} _executeRequest - Runs a request with authentication and a one-time retry on 401.
19
20
  */
20
21
 
21
22
  /**
@@ -82,8 +83,7 @@ export function createApi() {
82
83
  * @returns {Promise<Object>}
83
84
  */
84
85
  async get(url = "", auth = true, skipResponseParsing = false) {
85
- await this.preCheck(auth);
86
- return await this.createRestClient().get(url, auth, skipResponseParsing);
86
+ return await this._executeRequest(auth, () => this.createRestClient().get(url, auth, skipResponseParsing));
87
87
  },
88
88
 
89
89
  /**
@@ -97,8 +97,7 @@ export function createApi() {
97
97
  * @returns {Promise<Object>}
98
98
  */
99
99
  async put(url = "", payload = {}, auth = true, skipResponseParsing = false) {
100
- await this.preCheck(auth);
101
- return await this.createRestClient().put(url, payload, skipResponseParsing);
100
+ return await this._executeRequest(auth, () => this.createRestClient().put(url, payload, skipResponseParsing));
102
101
  },
103
102
 
104
103
  /**
@@ -112,8 +111,7 @@ export function createApi() {
112
111
  * @returns {Promise<Object>}
113
112
  */
114
113
  async post(url = "", payload = {}, auth = true, skipResponseParsing = false) {
115
- await this.preCheck(auth);
116
- return await this.createRestClient().post(url, payload, skipResponseParsing);
114
+ return await this._executeRequest(auth, () => this.createRestClient().post(url, payload, skipResponseParsing));
117
115
  },
118
116
 
119
117
  /**
@@ -127,8 +125,35 @@ export function createApi() {
127
125
  * @returns {Promise<Object>}
128
126
  */
129
127
  async delete(url = "", payload = null, auth = true, skipResponseParsing = false) {
128
+ return await this._executeRequest(auth, () => this.createRestClient().delete(url, payload, skipResponseParsing));
129
+ },
130
+
131
+ /**
132
+ * Runs a request with authentication and retries it once if the server
133
+ * answered 401 Unauthorized: the cached token is discarded, a fresh one
134
+ * is obtained via the auth manager and the request is repeated. This
135
+ * covers tokens that expired server-side (e.g. after hours of
136
+ * inactivity) as well as refresh races between parallel requests.
137
+ *
138
+ * @private
139
+ * @async
140
+ * @param {boolean} auth - Whether the request requires authentication.
141
+ * @param {() => Promise<any>} executeRequest - Performs the request; must create the rest client inside so a retry picks up the fresh token.
142
+ * @returns {Promise<any>}
143
+ */
144
+ async _executeRequest(auth, executeRequest) {
130
145
  await this.preCheck(auth);
131
- return await this.createRestClient().delete(url, payload, skipResponseParsing);
146
+ try {
147
+ return await executeRequest();
148
+ } catch (e) {
149
+ if (!auth || !this.authManager || !(e instanceof RestError) || e.status !== 401) {
150
+ throw e;
151
+ }
152
+
153
+ this.authManager.token = "";
154
+ await this.authManager.ensureAuthenticated();
155
+ return await executeRequest();
156
+ }
132
157
  },
133
158
 
134
159
  /**
@@ -23,6 +23,8 @@ import { popRefreshTokenFromUrl } from "./fluentAuthUtils";
23
23
  * @property {(storedRefreshToken?: string|null) => FluentAuthManager} useRefreshToken - Sets the refresh token and returns the FluentApi object.
24
24
  * @property {() => Promise<void>} ensureAuthenticated - Ensures the user is authenticated before making a request.
25
25
  * @property {() => Promise<void>} authenticate - Authenticates the user with username and password, or refreshes the token.
26
+ * @property {() => Promise<void>} _doAuthenticate - Performs the actual token refresh (single-flight worker behind authenticate).
27
+ * @property {Promise<void>|null} _authenticatePromise - In-flight authentication promise shared by concurrent callers.
26
28
  * @property {() => Promise<FluentAuthManager>} init - Returns promise for authManager.
27
29
  * @property {(username?: string, password?: string) => Promise<void>} login - Logs in with the provided credentials.
28
30
  * @property {(refreshToken?: string) => Promise<string|null>} tryRefreshToken - Attempts to refresh the authentication token using the refresh token.
@@ -45,6 +47,7 @@ export function createAuthManager() {
45
47
  token: "",
46
48
  refreshToken: "",
47
49
  userInfo: {},
50
+ _authenticatePromise: null,
48
51
 
49
52
  /**
50
53
  * app token to use for authentication
@@ -117,10 +120,35 @@ export function createAuthManager() {
117
120
  * Authenticates the user with the JWT token or refreshes the token with
118
121
  * the refreshToken set before.
119
122
  *
123
+ * Single-flight: concurrent callers (e.g. parallel requests firing after
124
+ * the token expired) share one refresh instead of racing each other with
125
+ * the same refresh token — with token rotation only the first refresh
126
+ * would succeed and all others would end up with a 401.
127
+ *
120
128
  * @throws {Error} if JWT token and refreshToken are not set or both are invalid
121
129
  * @return {Promise<void>}
122
130
  */
123
131
  async authenticate() { // benutzt bei existierendem JWT oder RefreshToken, wenn keins vorhanden ERROR
132
+ if (this.token && isTokenValid(this.token)) {
133
+ return;
134
+ }
135
+
136
+ this._authenticatePromise ??= this._doAuthenticate()
137
+ .finally(() => { this._authenticatePromise = null; });
138
+
139
+ return this._authenticatePromise;
140
+ },
141
+
142
+ /**
143
+ * Performs the actual authentication/refresh. Never call directly —
144
+ * always go through authenticate(), which ensures only one refresh
145
+ * runs at a time.
146
+ *
147
+ * @private
148
+ * @throws {Error} if JWT token and refreshToken are not set or both are invalid
149
+ * @return {Promise<void>}
150
+ */
151
+ async _doAuthenticate() {
124
152
  console.log("authenticating:", this.token ? `token set, exp: ${jwtDecode(this.token).exp - (Date.now() / 1000)}` : "no token,", this.refreshToken, this.appToken);
125
153
 
126
154
  if (this.token && isTokenValid(this.token)) {
@@ -14,6 +14,27 @@
14
14
  * @property {(res: Response) => Promise<any>} _parseReponse - Parses a response based on its content type.
15
15
  */
16
16
 
17
+ /**
18
+ * Error thrown for non-ok HTTP responses. Carries the status code so callers
19
+ * can react to specific statuses (e.g. the automatic 401 retry in fluentApi)
20
+ * without parsing the message.
21
+ */
22
+ export class RestError extends Error {
23
+ /**
24
+ * @param {string} method - HTTP method of the failed request.
25
+ * @param {string} url - Full URL of the failed request.
26
+ * @param {Response} res - The non-ok fetch response.
27
+ */
28
+ constructor(method, url, res) {
29
+ super(`${method} ${url} failed: ${res.status} ${res.statusText}`);
30
+ this.name = "RestError";
31
+ this.method = method;
32
+ this.url = url;
33
+ this.status = res.status;
34
+ this.statusText = res.statusText;
35
+ }
36
+ }
37
+
17
38
  /**
18
39
  * Creates a REST client object with fluent API for making HTTP requests.
19
40
  * @returns {FluentRESTClient} The REST client object.
@@ -71,7 +92,7 @@ export function restClient() {
71
92
  return skipResponseParsing ? res : await this._parseReponse(res);
72
93
  }
73
94
 
74
- throw new Error(`GET ${finalUrl} failed: ${res.status} ${res.statusText}`);
95
+ throw new RestError("GET", finalUrl, res);
75
96
  },
76
97
 
77
98
  /**
@@ -91,7 +112,7 @@ export function restClient() {
91
112
  return skipResponseParsing ? res : await this._parseReponse(res);
92
113
  }
93
114
 
94
- throw new Error(`PUT ${finalUrl} failed: ${res.status} ${res.statusText}`);
115
+ throw new RestError("PUT", finalUrl, res);
95
116
  },
96
117
 
97
118
  /**
@@ -122,7 +143,7 @@ export function restClient() {
122
143
  return skipResponseParsing ? res : await this._parseReponse(res);
123
144
  }
124
145
 
125
- throw new Error(`POST ${finalUrl} failed: ${res.status} ${res.statusText}`);
146
+ throw new RestError("POST", finalUrl, res);
126
147
  },
127
148
 
128
149
  /**
@@ -145,7 +166,7 @@ export function restClient() {
145
166
  return skipResponseParsing ? res : await this._parseReponse(res);
146
167
  }
147
168
 
148
- throw new Error(`DELETE ${finalUrl} failed: ${res.status} ${res.statusText}`);
169
+ throw new RestError("DELETE", finalUrl, res);
149
170
  },
150
171
 
151
172
  _createHeaders(contentType) {
package/index.d.ts CHANGED
@@ -7,6 +7,7 @@ export function createAuthManager(): FluentAuthManager;
7
7
  export function fluentIdasAuthManager(appToken: string, authBaseUrl: string): FluentAuthManager;
8
8
  export function fetchEnvConfig(envConfig?: string): Promise<EnvironmentConfig>;
9
9
  export function restClient(): FluentRESTClient;
10
+ export class RestError extends Error { method: string; url: string; status: number; statusText: string; constructor(method: string, url: string, res: Response); }
10
11
 
11
12
  export type AblageApi = {
12
13
  get: (guid: string) => Promise<AblageDTO>;
@@ -1120,6 +1121,7 @@ export type FluentApi = {
1120
1121
  delete: (url?: string, payload?: object|FormData|Array<any>|string[]|string|null, auth?: boolean) => Promise<object|Array<any>>;
1121
1122
  createRestClient: () => FluentRESTClient;
1122
1123
  preCheck: (auth?: boolean) => Promise<void>;
1124
+ _executeRequest: (auth: boolean, executeRequest: () => Promise<any>) => Promise<any>;
1123
1125
  };
1124
1126
 
1125
1127
  export type FluentAuthManager = {
@@ -1134,6 +1136,8 @@ export type FluentAuthManager = {
1134
1136
  useRefreshToken: (storedRefreshToken?: string|null) => FluentAuthManager;
1135
1137
  ensureAuthenticated: () => Promise<void>;
1136
1138
  authenticate: () => Promise<void>;
1139
+ _doAuthenticate: () => Promise<void>;
1140
+ _authenticatePromise: Promise<void>|null;
1137
1141
  init: () => Promise<FluentAuthManager>;
1138
1142
  login: (username?: string, password?: string) => Promise<void>;
1139
1143
  tryRefreshToken: (refreshToken?: string) => Promise<string|null>;
package/index.js CHANGED
@@ -2,7 +2,7 @@ export { createApi, fluentApi } from "./api/fluentApi";
2
2
  export { createIDASApi, idasFluentApi } from "./api/idasFluentApi";
3
3
  export { createAuthManager, fluentIdasAuthManager } from "./api/fluentAuthManager";
4
4
  export { fetchEnvConfig } from "./api/fluentEnvUtils";
5
- export { restClient } from "./api/fluentRestClient";
5
+ export { restClient, RestError } from "./api/fluentRestClient";
6
6
 
7
7
  // Business Routines APIs
8
8
  export * from "./api/business/index.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gandalan/weblibs",
3
- "version": "2.0.7",
3
+ "version": "2.0.8",
4
4
  "description": "WebLibs for Gandalan JS/TS projects",
5
5
  "keywords": [
6
6
  "gandalan"
@@ -31,7 +31,8 @@ const rootFunctionDeclarationStatements = [
31
31
  "export function createAuthManager(): FluentAuthManager;",
32
32
  "export function fluentIdasAuthManager(appToken: string, authBaseUrl: string): FluentAuthManager;",
33
33
  "export function fetchEnvConfig(envConfig?: string): Promise<EnvironmentConfig>;",
34
- "export function restClient(): FluentRESTClient;"
34
+ "export function restClient(): FluentRESTClient;",
35
+ "export class RestError extends Error { method: string; url: string; status: number; statusText: string; constructor(method: string, url: string, res: Response); }"
35
36
  ];
36
37
 
37
38
  const simpleImportTypePattern = /^import\((?:"|').+(?:"|')\)\.[A-Za-z0-9_$]+$/;