fetchguard 1.6.3 → 2.1.0
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 +268 -66
- package/dist/index.d.ts +594 -31
- package/dist/index.js +544 -86
- package/dist/index.js.map +1 -1
- package/dist/worker.js +177 -74
- package/dist/worker.js.map +1 -1
- package/package.json +8 -3
package/dist/worker.js
CHANGED
|
@@ -18,7 +18,8 @@ var MSG = Object.freeze({
|
|
|
18
18
|
AUTH_STATE_CHANGED: "AUTH_STATE_CHANGED",
|
|
19
19
|
AUTH_CALL_RESULT: "AUTH_CALL_RESULT",
|
|
20
20
|
FETCH_RESULT: "FETCH_RESULT",
|
|
21
|
-
FETCH_ERROR: "FETCH_ERROR"
|
|
21
|
+
FETCH_ERROR: "FETCH_ERROR",
|
|
22
|
+
TOKEN_REFRESHED: "TOKEN_REFRESHED"
|
|
22
23
|
});
|
|
23
24
|
|
|
24
25
|
// src/constants.ts
|
|
@@ -26,33 +27,67 @@ var DEFAULT_REFRESH_EARLY_MS = 6e4;
|
|
|
26
27
|
|
|
27
28
|
// src/errors.ts
|
|
28
29
|
import { defineError, defineErrorAdvanced } from "ts-micro-result";
|
|
30
|
+
|
|
31
|
+
// src/error-codes.ts
|
|
32
|
+
var ERROR_CODES = {
|
|
33
|
+
// General
|
|
34
|
+
UNEXPECTED: "UNEXPECTED",
|
|
35
|
+
UNKNOWN_MESSAGE: "UNKNOWN_MESSAGE",
|
|
36
|
+
RESULT_PARSE_ERROR: "RESULT_PARSE_ERROR",
|
|
37
|
+
// Init
|
|
38
|
+
INIT_ERROR: "INIT_ERROR",
|
|
39
|
+
PROVIDER_INIT_FAILED: "PROVIDER_INIT_FAILED",
|
|
40
|
+
INIT_FAILED: "INIT_FAILED",
|
|
41
|
+
// Auth
|
|
42
|
+
TOKEN_REFRESH_FAILED: "TOKEN_REFRESH_FAILED",
|
|
43
|
+
TOKEN_EXCHANGE_FAILED: "TOKEN_EXCHANGE_FAILED",
|
|
44
|
+
LOGIN_FAILED: "LOGIN_FAILED",
|
|
45
|
+
LOGOUT_FAILED: "LOGOUT_FAILED",
|
|
46
|
+
NOT_AUTHENTICATED: "NOT_AUTHENTICATED",
|
|
47
|
+
// Domain
|
|
48
|
+
DOMAIN_NOT_ALLOWED: "DOMAIN_NOT_ALLOWED",
|
|
49
|
+
// Request
|
|
50
|
+
NETWORK_ERROR: "NETWORK_ERROR",
|
|
51
|
+
REQUEST_CANCELLED: "REQUEST_CANCELLED",
|
|
52
|
+
HTTP_ERROR: "HTTP_ERROR",
|
|
53
|
+
RESPONSE_PARSE_FAILED: "RESPONSE_PARSE_FAILED",
|
|
54
|
+
QUEUE_FULL: "QUEUE_FULL",
|
|
55
|
+
REQUEST_TIMEOUT: "REQUEST_TIMEOUT"
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// src/errors.ts
|
|
29
59
|
var GeneralErrors = {
|
|
30
|
-
Unexpected: defineError(
|
|
31
|
-
UnknownMessage: defineError(
|
|
32
|
-
ResultParse: defineError(
|
|
60
|
+
Unexpected: defineError(ERROR_CODES.UNEXPECTED, "Unexpected error"),
|
|
61
|
+
UnknownMessage: defineError(ERROR_CODES.UNKNOWN_MESSAGE, "Unknown message type"),
|
|
62
|
+
ResultParse: defineError(ERROR_CODES.RESULT_PARSE_ERROR, "Failed to parse result")
|
|
33
63
|
};
|
|
34
64
|
var InitErrors = {
|
|
35
|
-
NotInitialized: defineError(
|
|
36
|
-
ProviderInitFailed: defineError(
|
|
37
|
-
InitFailed: defineError(
|
|
65
|
+
NotInitialized: defineError(ERROR_CODES.INIT_ERROR, "Worker not initialized"),
|
|
66
|
+
ProviderInitFailed: defineError(ERROR_CODES.PROVIDER_INIT_FAILED, "Failed to initialize provider"),
|
|
67
|
+
InitFailed: defineError(ERROR_CODES.INIT_FAILED, "Initialization failed")
|
|
38
68
|
};
|
|
39
69
|
var AuthErrors = {
|
|
40
|
-
TokenRefreshFailed: defineError(
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
70
|
+
TokenRefreshFailed: defineError(ERROR_CODES.TOKEN_REFRESH_FAILED, "Token refresh failed"),
|
|
71
|
+
TokenExchangeFailed: defineError(ERROR_CODES.TOKEN_EXCHANGE_FAILED, "Token exchange failed"),
|
|
72
|
+
LoginFailed: defineError(ERROR_CODES.LOGIN_FAILED, "Login failed"),
|
|
73
|
+
LogoutFailed: defineError(ERROR_CODES.LOGOUT_FAILED, "Logout failed"),
|
|
74
|
+
NotAuthenticated: defineError(ERROR_CODES.NOT_AUTHENTICATED, "User is not authenticated")
|
|
44
75
|
};
|
|
45
76
|
var DomainErrors = {
|
|
46
|
-
NotAllowed: defineErrorAdvanced(
|
|
77
|
+
NotAllowed: defineErrorAdvanced(ERROR_CODES.DOMAIN_NOT_ALLOWED, "Domain not allowed: {url}")
|
|
47
78
|
};
|
|
48
79
|
var RequestErrors = {
|
|
49
80
|
// Network errors (connection failed, no response)
|
|
50
|
-
NetworkError: defineError(
|
|
51
|
-
Cancelled: defineError(
|
|
81
|
+
NetworkError: defineError(ERROR_CODES.NETWORK_ERROR, "Network error"),
|
|
82
|
+
Cancelled: defineError(ERROR_CODES.REQUEST_CANCELLED, "Request was cancelled"),
|
|
52
83
|
// HTTP errors (server responded with error status)
|
|
53
|
-
HttpError: defineErrorAdvanced(
|
|
84
|
+
HttpError: defineErrorAdvanced(ERROR_CODES.HTTP_ERROR, "HTTP {status} error"),
|
|
54
85
|
// Response parsing errors
|
|
55
|
-
ResponseParseFailed: defineError(
|
|
86
|
+
ResponseParseFailed: defineError(ERROR_CODES.RESPONSE_PARSE_FAILED, "Failed to parse response body"),
|
|
87
|
+
// Queue errors
|
|
88
|
+
QueueFull: defineErrorAdvanced(ERROR_CODES.QUEUE_FULL, "Request queue full ({size}/{maxSize})"),
|
|
89
|
+
// Timeout errors
|
|
90
|
+
Timeout: defineError(ERROR_CODES.REQUEST_TIMEOUT, "Request timed out")
|
|
56
91
|
};
|
|
57
92
|
|
|
58
93
|
// src/worker-post.ts
|
|
@@ -61,17 +96,19 @@ function post(message) {
|
|
|
61
96
|
self.postMessage(message);
|
|
62
97
|
}
|
|
63
98
|
function sendError(id, result) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
99
|
+
if (!result.ok) {
|
|
100
|
+
post({
|
|
101
|
+
type: MSG.ERROR,
|
|
102
|
+
id,
|
|
103
|
+
payload: { errors: result.errors, meta: result.meta }
|
|
104
|
+
});
|
|
105
|
+
}
|
|
69
106
|
}
|
|
70
|
-
function sendFetchResult(id,
|
|
107
|
+
function sendFetchResult(id, envelope) {
|
|
71
108
|
post({
|
|
72
109
|
type: MSG.FETCH_RESULT,
|
|
73
110
|
id,
|
|
74
|
-
payload:
|
|
111
|
+
payload: envelope
|
|
75
112
|
});
|
|
76
113
|
}
|
|
77
114
|
function sendFetchError(id, error, status) {
|
|
@@ -115,6 +152,13 @@ function sendAuthCallResult(id, authResult) {
|
|
|
115
152
|
payload: authResult
|
|
116
153
|
});
|
|
117
154
|
}
|
|
155
|
+
function sendTokenRefreshed(reason) {
|
|
156
|
+
post({
|
|
157
|
+
type: MSG.TOKEN_REFRESHED,
|
|
158
|
+
id: `evt_${Date.now()}`,
|
|
159
|
+
payload: { reason }
|
|
160
|
+
});
|
|
161
|
+
}
|
|
118
162
|
|
|
119
163
|
// src/utils/registry.ts
|
|
120
164
|
var registry = /* @__PURE__ */ new Map();
|
|
@@ -139,7 +183,7 @@ function createProvider(config) {
|
|
|
139
183
|
const response = await config.strategy.refresh(currentRefreshToken);
|
|
140
184
|
if (!response.ok) {
|
|
141
185
|
const body = await response.text().catch(() => "");
|
|
142
|
-
return err(AuthErrors.TokenRefreshFailed(), { body
|
|
186
|
+
return err(AuthErrors.TokenRefreshFailed(), { params: { body, status: response.status } });
|
|
143
187
|
}
|
|
144
188
|
const tokenInfo = await config.parser.parse(response);
|
|
145
189
|
if (!tokenInfo.token) {
|
|
@@ -158,7 +202,7 @@ function createProvider(config) {
|
|
|
158
202
|
const response = await config.strategy.login(payload, url);
|
|
159
203
|
if (!response.ok) {
|
|
160
204
|
const body = await response.text().catch(() => "");
|
|
161
|
-
return err(AuthErrors.LoginFailed(), { body
|
|
205
|
+
return err(AuthErrors.LoginFailed(), { params: { body, status: response.status } });
|
|
162
206
|
}
|
|
163
207
|
const tokenInfo = await config.parser.parse(response);
|
|
164
208
|
if (!tokenInfo.token) {
|
|
@@ -177,7 +221,7 @@ function createProvider(config) {
|
|
|
177
221
|
const response = await config.strategy.logout(payload);
|
|
178
222
|
if (!response.ok) {
|
|
179
223
|
const body = await response.text().catch(() => "");
|
|
180
|
-
return err(AuthErrors.LogoutFailed(), { body
|
|
224
|
+
return err(AuthErrors.LogoutFailed(), { params: { body, status: response.status } });
|
|
181
225
|
}
|
|
182
226
|
if (config.refreshStorage) {
|
|
183
227
|
await config.refreshStorage.set(null);
|
|
@@ -192,6 +236,37 @@ function createProvider(config) {
|
|
|
192
236
|
} catch (error) {
|
|
193
237
|
return err(RequestErrors.NetworkError({ message: String(error) }));
|
|
194
238
|
}
|
|
239
|
+
},
|
|
240
|
+
async exchangeToken(accessToken, url, options = {}) {
|
|
241
|
+
const { method = "POST", payload } = options;
|
|
242
|
+
if (!accessToken) {
|
|
243
|
+
return err(AuthErrors.NotAuthenticated());
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
const response = await fetch(url, {
|
|
247
|
+
method,
|
|
248
|
+
headers: {
|
|
249
|
+
"Content-Type": "application/json",
|
|
250
|
+
"Authorization": `Bearer ${accessToken}`
|
|
251
|
+
},
|
|
252
|
+
body: payload ? JSON.stringify(payload) : void 0,
|
|
253
|
+
credentials: "include"
|
|
254
|
+
});
|
|
255
|
+
if (!response.ok) {
|
|
256
|
+
const body = await response.text().catch(() => "");
|
|
257
|
+
return err(AuthErrors.TokenExchangeFailed(), { params: { body, status: response.status } });
|
|
258
|
+
}
|
|
259
|
+
const tokenInfo = await config.parser.parse(response);
|
|
260
|
+
if (!tokenInfo.token) {
|
|
261
|
+
return err(AuthErrors.TokenExchangeFailed({ message: "No access token in response" }));
|
|
262
|
+
}
|
|
263
|
+
if (config.refreshStorage && tokenInfo.refreshToken) {
|
|
264
|
+
await config.refreshStorage.set(tokenInfo.refreshToken);
|
|
265
|
+
}
|
|
266
|
+
return ok(tokenInfo);
|
|
267
|
+
} catch (error) {
|
|
268
|
+
return err(RequestErrors.NetworkError({ message: String(error) }));
|
|
269
|
+
}
|
|
195
270
|
}
|
|
196
271
|
};
|
|
197
272
|
if (config.customMethods) {
|
|
@@ -204,7 +279,13 @@ function createProvider(config) {
|
|
|
204
279
|
}
|
|
205
280
|
|
|
206
281
|
// src/provider/storage/indexeddb.ts
|
|
207
|
-
function createIndexedDBStorage(
|
|
282
|
+
function createIndexedDBStorage(options = "FetchGuardDB", legacyRefreshTokenKey) {
|
|
283
|
+
const config = typeof options === "string" ? { dbName: options, refreshTokenKey: legacyRefreshTokenKey ?? "refreshToken", onError: void 0 } : {
|
|
284
|
+
dbName: options.dbName ?? "FetchGuardDB",
|
|
285
|
+
refreshTokenKey: options.refreshTokenKey ?? "refreshToken",
|
|
286
|
+
onError: options.onError
|
|
287
|
+
};
|
|
288
|
+
const { dbName, refreshTokenKey, onError } = config;
|
|
208
289
|
const storeName = "tokens";
|
|
209
290
|
const openDB = () => {
|
|
210
291
|
return new Promise((resolve, reject) => {
|
|
@@ -235,7 +316,7 @@ function createIndexedDBStorage(dbName = "FetchGuardDB", refreshTokenKey = "refr
|
|
|
235
316
|
const result = await promisifyRequest(store.get(refreshTokenKey));
|
|
236
317
|
return result?.value || null;
|
|
237
318
|
} catch (error) {
|
|
238
|
-
|
|
319
|
+
onError?.(error, "get");
|
|
239
320
|
return null;
|
|
240
321
|
}
|
|
241
322
|
},
|
|
@@ -250,7 +331,7 @@ function createIndexedDBStorage(dbName = "FetchGuardDB", refreshTokenKey = "refr
|
|
|
250
331
|
await promisifyRequest(store.delete(refreshTokenKey));
|
|
251
332
|
}
|
|
252
333
|
} catch (error) {
|
|
253
|
-
|
|
334
|
+
onError?.(error, token ? "set" : "delete");
|
|
254
335
|
}
|
|
255
336
|
}
|
|
256
337
|
};
|
|
@@ -424,8 +505,7 @@ function deserializeFormData(serialized) {
|
|
|
424
505
|
if (typeof value === "string") {
|
|
425
506
|
formData.append(key, value);
|
|
426
507
|
} else {
|
|
427
|
-
const
|
|
428
|
-
const file = new File([uint8Array], value.name, { type: value.type });
|
|
508
|
+
const file = new File([value.buffer], value.name, { type: value.type });
|
|
429
509
|
formData.append(key, file);
|
|
430
510
|
}
|
|
431
511
|
}
|
|
@@ -465,16 +545,33 @@ function isBinaryContentType(contentType) {
|
|
|
465
545
|
let currentUser;
|
|
466
546
|
const pendingControllers = /* @__PURE__ */ new Map();
|
|
467
547
|
let refreshPromise = null;
|
|
548
|
+
let authPromise = null;
|
|
549
|
+
async function withAuthMutex(operation) {
|
|
550
|
+
if (authPromise) {
|
|
551
|
+
try {
|
|
552
|
+
await authPromise;
|
|
553
|
+
} catch {
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
const promise = operation();
|
|
557
|
+
authPromise = promise;
|
|
558
|
+
try {
|
|
559
|
+
return await promise;
|
|
560
|
+
} finally {
|
|
561
|
+
authPromise = null;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
468
564
|
async function ensureValidToken() {
|
|
469
565
|
if (!provider) {
|
|
470
566
|
return err2(InitErrors.NotInitialized());
|
|
471
567
|
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
568
|
+
const refreshEarlyMs = config?.refreshEarlyMs ?? DEFAULT_REFRESH_EARLY_MS;
|
|
569
|
+
const now = Date.now();
|
|
570
|
+
const timeLeft = expiresAt ? expiresAt - now : 0;
|
|
571
|
+
const isExpired = !accessToken || !expiresAt || timeLeft <= 0;
|
|
572
|
+
const isProactive = !isExpired && timeLeft <= refreshEarlyMs;
|
|
573
|
+
if (accessToken && expiresAt && timeLeft > refreshEarlyMs) {
|
|
574
|
+
return ok2(accessToken);
|
|
478
575
|
}
|
|
479
576
|
if (refreshPromise) {
|
|
480
577
|
const result = await refreshPromise;
|
|
@@ -486,7 +583,7 @@ function isBinaryContentType(contentType) {
|
|
|
486
583
|
return err2(InitErrors.NotInitialized());
|
|
487
584
|
}
|
|
488
585
|
const valueRes = await provider.refreshToken(refreshToken);
|
|
489
|
-
if (valueRes.
|
|
586
|
+
if (!valueRes.ok) {
|
|
490
587
|
setTokenState({ token: null, expiresAt: null, user: void 0, refreshToken: void 0 });
|
|
491
588
|
return err2(valueRes.errors);
|
|
492
589
|
}
|
|
@@ -498,6 +595,7 @@ function isBinaryContentType(contentType) {
|
|
|
498
595
|
if (!accessToken) {
|
|
499
596
|
return err2(AuthErrors.TokenRefreshFailed({ message: "Access token is null after refresh" }));
|
|
500
597
|
}
|
|
598
|
+
sendTokenRefreshed(isProactive ? "proactive" : "expired");
|
|
501
599
|
return ok2(accessToken);
|
|
502
600
|
} finally {
|
|
503
601
|
refreshPromise = null;
|
|
@@ -557,7 +655,7 @@ function isBinaryContentType(contentType) {
|
|
|
557
655
|
}
|
|
558
656
|
if (requiresAuth) {
|
|
559
657
|
const tokenRes = await ensureValidToken();
|
|
560
|
-
if (tokenRes.
|
|
658
|
+
if (!tokenRes.ok) {
|
|
561
659
|
return err2(tokenRes.errors);
|
|
562
660
|
}
|
|
563
661
|
const token = tokenRes.data;
|
|
@@ -656,13 +754,12 @@ function isBinaryContentType(contentType) {
|
|
|
656
754
|
pendingControllers.set(id, controller);
|
|
657
755
|
const merged = { ...options || {}, signal: controller.signal };
|
|
658
756
|
const result = await makeApiRequest(url, merged);
|
|
659
|
-
if (result.
|
|
757
|
+
if (result.ok) {
|
|
660
758
|
sendFetchResult(id, result.data);
|
|
661
759
|
} else {
|
|
662
|
-
const error = result.errors
|
|
760
|
+
const error = result.errors[0];
|
|
663
761
|
const message = error?.message || "Unknown error";
|
|
664
|
-
|
|
665
|
-
sendFetchError(id, message, status);
|
|
762
|
+
sendFetchError(id, message, void 0);
|
|
666
763
|
}
|
|
667
764
|
pendingControllers.delete(id);
|
|
668
765
|
} catch (error) {
|
|
@@ -673,38 +770,44 @@ function isBinaryContentType(contentType) {
|
|
|
673
770
|
}
|
|
674
771
|
case MSG.AUTH_CALL: {
|
|
675
772
|
const { id, payload } = data;
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
773
|
+
await withAuthMutex(async () => {
|
|
774
|
+
try {
|
|
775
|
+
const { method, args, emitEvent } = payload;
|
|
776
|
+
const shouldEmitEvent = emitEvent ?? true;
|
|
777
|
+
if (!provider) {
|
|
778
|
+
sendError(id, err2(InitErrors.NotInitialized()));
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
if (typeof provider[method] !== "function") {
|
|
782
|
+
sendError(id, err2(GeneralErrors.Unexpected({ message: `Method '${method}' not found on provider` })));
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
let methodArgs = args;
|
|
786
|
+
if (method === "exchangeToken") {
|
|
787
|
+
methodArgs = [accessToken, ...args];
|
|
788
|
+
}
|
|
789
|
+
const result = await provider[method](...methodArgs);
|
|
790
|
+
if (!result.ok) {
|
|
791
|
+
sendError(id, result);
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
const tokenInfo = result.data;
|
|
795
|
+
if (!tokenInfo) {
|
|
796
|
+
sendError(id, err2(GeneralErrors.Unexpected({ message: "Provider returned null token info" })));
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
setTokenState(tokenInfo, shouldEmitEvent);
|
|
800
|
+
const now = Date.now();
|
|
801
|
+
const authenticated = accessToken !== null && accessToken !== "" && (expiresAt === null || expiresAt > now);
|
|
802
|
+
sendAuthCallResult(id, {
|
|
803
|
+
authenticated,
|
|
804
|
+
expiresAt,
|
|
805
|
+
user: currentUser
|
|
806
|
+
});
|
|
807
|
+
} catch (error) {
|
|
808
|
+
sendError(id, err2(GeneralErrors.Unexpected({ message: error instanceof Error ? error.message : String(error) })));
|
|
691
809
|
}
|
|
692
|
-
|
|
693
|
-
if (!tokenInfo) {
|
|
694
|
-
sendError(id, err2(GeneralErrors.Unexpected({ message: "Provider returned null token info" })));
|
|
695
|
-
break;
|
|
696
|
-
}
|
|
697
|
-
setTokenState(tokenInfo, shouldEmitEvent);
|
|
698
|
-
const now = Date.now();
|
|
699
|
-
const authenticated = accessToken !== null && accessToken !== "" && (expiresAt === null || expiresAt > now);
|
|
700
|
-
sendAuthCallResult(id, {
|
|
701
|
-
authenticated,
|
|
702
|
-
expiresAt,
|
|
703
|
-
user: currentUser
|
|
704
|
-
});
|
|
705
|
-
} catch (error) {
|
|
706
|
-
sendError(id, err2(GeneralErrors.Unexpected({ message: error instanceof Error ? error.message : String(error) })));
|
|
707
|
-
}
|
|
810
|
+
});
|
|
708
811
|
break;
|
|
709
812
|
}
|
|
710
813
|
case MSG.CANCEL: {
|