@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 +33 -8
- package/api/fluentAuthManager.js +28 -0
- package/api/fluentRestClient.js +25 -4
- package/index.d.ts +4 -0
- package/index.js +1 -1
- package/package.json +1 -1
- package/scripts/generate-dts.mjs +2 -1
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
/**
|
package/api/fluentAuthManager.js
CHANGED
|
@@ -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)) {
|
package/api/fluentRestClient.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
package/scripts/generate-dts.mjs
CHANGED
|
@@ -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_$]+$/;
|