fetchguard 1.6.3 → 2.0.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 +235 -65
- package/dist/index.d.ts +540 -31
- package/dist/index.js +480 -86
- package/dist/index.js.map +1 -1
- package/dist/worker.js +140 -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,65 @@ 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
|
+
LOGIN_FAILED: "LOGIN_FAILED",
|
|
44
|
+
LOGOUT_FAILED: "LOGOUT_FAILED",
|
|
45
|
+
NOT_AUTHENTICATED: "NOT_AUTHENTICATED",
|
|
46
|
+
// Domain
|
|
47
|
+
DOMAIN_NOT_ALLOWED: "DOMAIN_NOT_ALLOWED",
|
|
48
|
+
// Request
|
|
49
|
+
NETWORK_ERROR: "NETWORK_ERROR",
|
|
50
|
+
REQUEST_CANCELLED: "REQUEST_CANCELLED",
|
|
51
|
+
HTTP_ERROR: "HTTP_ERROR",
|
|
52
|
+
RESPONSE_PARSE_FAILED: "RESPONSE_PARSE_FAILED",
|
|
53
|
+
QUEUE_FULL: "QUEUE_FULL",
|
|
54
|
+
REQUEST_TIMEOUT: "REQUEST_TIMEOUT"
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// src/errors.ts
|
|
29
58
|
var GeneralErrors = {
|
|
30
|
-
Unexpected: defineError(
|
|
31
|
-
UnknownMessage: defineError(
|
|
32
|
-
ResultParse: defineError(
|
|
59
|
+
Unexpected: defineError(ERROR_CODES.UNEXPECTED, "Unexpected error"),
|
|
60
|
+
UnknownMessage: defineError(ERROR_CODES.UNKNOWN_MESSAGE, "Unknown message type"),
|
|
61
|
+
ResultParse: defineError(ERROR_CODES.RESULT_PARSE_ERROR, "Failed to parse result")
|
|
33
62
|
};
|
|
34
63
|
var InitErrors = {
|
|
35
|
-
NotInitialized: defineError(
|
|
36
|
-
ProviderInitFailed: defineError(
|
|
37
|
-
InitFailed: defineError(
|
|
64
|
+
NotInitialized: defineError(ERROR_CODES.INIT_ERROR, "Worker not initialized"),
|
|
65
|
+
ProviderInitFailed: defineError(ERROR_CODES.PROVIDER_INIT_FAILED, "Failed to initialize provider"),
|
|
66
|
+
InitFailed: defineError(ERROR_CODES.INIT_FAILED, "Initialization failed")
|
|
38
67
|
};
|
|
39
68
|
var AuthErrors = {
|
|
40
|
-
TokenRefreshFailed: defineError(
|
|
41
|
-
LoginFailed: defineError(
|
|
42
|
-
LogoutFailed: defineError(
|
|
43
|
-
NotAuthenticated: defineError(
|
|
69
|
+
TokenRefreshFailed: defineError(ERROR_CODES.TOKEN_REFRESH_FAILED, "Token refresh failed"),
|
|
70
|
+
LoginFailed: defineError(ERROR_CODES.LOGIN_FAILED, "Login failed"),
|
|
71
|
+
LogoutFailed: defineError(ERROR_CODES.LOGOUT_FAILED, "Logout failed"),
|
|
72
|
+
NotAuthenticated: defineError(ERROR_CODES.NOT_AUTHENTICATED, "User is not authenticated")
|
|
44
73
|
};
|
|
45
74
|
var DomainErrors = {
|
|
46
|
-
NotAllowed: defineErrorAdvanced(
|
|
75
|
+
NotAllowed: defineErrorAdvanced(ERROR_CODES.DOMAIN_NOT_ALLOWED, "Domain not allowed: {url}")
|
|
47
76
|
};
|
|
48
77
|
var RequestErrors = {
|
|
49
78
|
// Network errors (connection failed, no response)
|
|
50
|
-
NetworkError: defineError(
|
|
51
|
-
Cancelled: defineError(
|
|
79
|
+
NetworkError: defineError(ERROR_CODES.NETWORK_ERROR, "Network error"),
|
|
80
|
+
Cancelled: defineError(ERROR_CODES.REQUEST_CANCELLED, "Request was cancelled"),
|
|
52
81
|
// HTTP errors (server responded with error status)
|
|
53
|
-
HttpError: defineErrorAdvanced(
|
|
82
|
+
HttpError: defineErrorAdvanced(ERROR_CODES.HTTP_ERROR, "HTTP {status} error"),
|
|
54
83
|
// Response parsing errors
|
|
55
|
-
ResponseParseFailed: defineError(
|
|
84
|
+
ResponseParseFailed: defineError(ERROR_CODES.RESPONSE_PARSE_FAILED, "Failed to parse response body"),
|
|
85
|
+
// Queue errors
|
|
86
|
+
QueueFull: defineErrorAdvanced(ERROR_CODES.QUEUE_FULL, "Request queue full ({size}/{maxSize})"),
|
|
87
|
+
// Timeout errors
|
|
88
|
+
Timeout: defineError(ERROR_CODES.REQUEST_TIMEOUT, "Request timed out")
|
|
56
89
|
};
|
|
57
90
|
|
|
58
91
|
// src/worker-post.ts
|
|
@@ -61,17 +94,19 @@ function post(message) {
|
|
|
61
94
|
self.postMessage(message);
|
|
62
95
|
}
|
|
63
96
|
function sendError(id, result) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
97
|
+
if (!result.ok) {
|
|
98
|
+
post({
|
|
99
|
+
type: MSG.ERROR,
|
|
100
|
+
id,
|
|
101
|
+
payload: { errors: result.errors, meta: result.meta }
|
|
102
|
+
});
|
|
103
|
+
}
|
|
69
104
|
}
|
|
70
|
-
function sendFetchResult(id,
|
|
105
|
+
function sendFetchResult(id, envelope) {
|
|
71
106
|
post({
|
|
72
107
|
type: MSG.FETCH_RESULT,
|
|
73
108
|
id,
|
|
74
|
-
payload:
|
|
109
|
+
payload: envelope
|
|
75
110
|
});
|
|
76
111
|
}
|
|
77
112
|
function sendFetchError(id, error, status) {
|
|
@@ -115,6 +150,13 @@ function sendAuthCallResult(id, authResult) {
|
|
|
115
150
|
payload: authResult
|
|
116
151
|
});
|
|
117
152
|
}
|
|
153
|
+
function sendTokenRefreshed(reason) {
|
|
154
|
+
post({
|
|
155
|
+
type: MSG.TOKEN_REFRESHED,
|
|
156
|
+
id: `evt_${Date.now()}`,
|
|
157
|
+
payload: { reason }
|
|
158
|
+
});
|
|
159
|
+
}
|
|
118
160
|
|
|
119
161
|
// src/utils/registry.ts
|
|
120
162
|
var registry = /* @__PURE__ */ new Map();
|
|
@@ -139,7 +181,7 @@ function createProvider(config) {
|
|
|
139
181
|
const response = await config.strategy.refresh(currentRefreshToken);
|
|
140
182
|
if (!response.ok) {
|
|
141
183
|
const body = await response.text().catch(() => "");
|
|
142
|
-
return err(AuthErrors.TokenRefreshFailed(), { body
|
|
184
|
+
return err(AuthErrors.TokenRefreshFailed(), { params: { body, status: response.status } });
|
|
143
185
|
}
|
|
144
186
|
const tokenInfo = await config.parser.parse(response);
|
|
145
187
|
if (!tokenInfo.token) {
|
|
@@ -158,7 +200,7 @@ function createProvider(config) {
|
|
|
158
200
|
const response = await config.strategy.login(payload, url);
|
|
159
201
|
if (!response.ok) {
|
|
160
202
|
const body = await response.text().catch(() => "");
|
|
161
|
-
return err(AuthErrors.LoginFailed(), { body
|
|
203
|
+
return err(AuthErrors.LoginFailed(), { params: { body, status: response.status } });
|
|
162
204
|
}
|
|
163
205
|
const tokenInfo = await config.parser.parse(response);
|
|
164
206
|
if (!tokenInfo.token) {
|
|
@@ -177,7 +219,7 @@ function createProvider(config) {
|
|
|
177
219
|
const response = await config.strategy.logout(payload);
|
|
178
220
|
if (!response.ok) {
|
|
179
221
|
const body = await response.text().catch(() => "");
|
|
180
|
-
return err(AuthErrors.LogoutFailed(), { body
|
|
222
|
+
return err(AuthErrors.LogoutFailed(), { params: { body, status: response.status } });
|
|
181
223
|
}
|
|
182
224
|
if (config.refreshStorage) {
|
|
183
225
|
await config.refreshStorage.set(null);
|
|
@@ -204,7 +246,13 @@ function createProvider(config) {
|
|
|
204
246
|
}
|
|
205
247
|
|
|
206
248
|
// src/provider/storage/indexeddb.ts
|
|
207
|
-
function createIndexedDBStorage(
|
|
249
|
+
function createIndexedDBStorage(options = "FetchGuardDB", legacyRefreshTokenKey) {
|
|
250
|
+
const config = typeof options === "string" ? { dbName: options, refreshTokenKey: legacyRefreshTokenKey ?? "refreshToken", onError: void 0 } : {
|
|
251
|
+
dbName: options.dbName ?? "FetchGuardDB",
|
|
252
|
+
refreshTokenKey: options.refreshTokenKey ?? "refreshToken",
|
|
253
|
+
onError: options.onError
|
|
254
|
+
};
|
|
255
|
+
const { dbName, refreshTokenKey, onError } = config;
|
|
208
256
|
const storeName = "tokens";
|
|
209
257
|
const openDB = () => {
|
|
210
258
|
return new Promise((resolve, reject) => {
|
|
@@ -235,7 +283,7 @@ function createIndexedDBStorage(dbName = "FetchGuardDB", refreshTokenKey = "refr
|
|
|
235
283
|
const result = await promisifyRequest(store.get(refreshTokenKey));
|
|
236
284
|
return result?.value || null;
|
|
237
285
|
} catch (error) {
|
|
238
|
-
|
|
286
|
+
onError?.(error, "get");
|
|
239
287
|
return null;
|
|
240
288
|
}
|
|
241
289
|
},
|
|
@@ -250,7 +298,7 @@ function createIndexedDBStorage(dbName = "FetchGuardDB", refreshTokenKey = "refr
|
|
|
250
298
|
await promisifyRequest(store.delete(refreshTokenKey));
|
|
251
299
|
}
|
|
252
300
|
} catch (error) {
|
|
253
|
-
|
|
301
|
+
onError?.(error, token ? "set" : "delete");
|
|
254
302
|
}
|
|
255
303
|
}
|
|
256
304
|
};
|
|
@@ -424,8 +472,7 @@ function deserializeFormData(serialized) {
|
|
|
424
472
|
if (typeof value === "string") {
|
|
425
473
|
formData.append(key, value);
|
|
426
474
|
} else {
|
|
427
|
-
const
|
|
428
|
-
const file = new File([uint8Array], value.name, { type: value.type });
|
|
475
|
+
const file = new File([value.buffer], value.name, { type: value.type });
|
|
429
476
|
formData.append(key, file);
|
|
430
477
|
}
|
|
431
478
|
}
|
|
@@ -465,16 +512,33 @@ function isBinaryContentType(contentType) {
|
|
|
465
512
|
let currentUser;
|
|
466
513
|
const pendingControllers = /* @__PURE__ */ new Map();
|
|
467
514
|
let refreshPromise = null;
|
|
515
|
+
let authPromise = null;
|
|
516
|
+
async function withAuthMutex(operation) {
|
|
517
|
+
if (authPromise) {
|
|
518
|
+
try {
|
|
519
|
+
await authPromise;
|
|
520
|
+
} catch {
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
const promise = operation();
|
|
524
|
+
authPromise = promise;
|
|
525
|
+
try {
|
|
526
|
+
return await promise;
|
|
527
|
+
} finally {
|
|
528
|
+
authPromise = null;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
468
531
|
async function ensureValidToken() {
|
|
469
532
|
if (!provider) {
|
|
470
533
|
return err2(InitErrors.NotInitialized());
|
|
471
534
|
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
535
|
+
const refreshEarlyMs = config?.refreshEarlyMs ?? DEFAULT_REFRESH_EARLY_MS;
|
|
536
|
+
const now = Date.now();
|
|
537
|
+
const timeLeft = expiresAt ? expiresAt - now : 0;
|
|
538
|
+
const isExpired = !accessToken || !expiresAt || timeLeft <= 0;
|
|
539
|
+
const isProactive = !isExpired && timeLeft <= refreshEarlyMs;
|
|
540
|
+
if (accessToken && expiresAt && timeLeft > refreshEarlyMs) {
|
|
541
|
+
return ok2(accessToken);
|
|
478
542
|
}
|
|
479
543
|
if (refreshPromise) {
|
|
480
544
|
const result = await refreshPromise;
|
|
@@ -486,7 +550,7 @@ function isBinaryContentType(contentType) {
|
|
|
486
550
|
return err2(InitErrors.NotInitialized());
|
|
487
551
|
}
|
|
488
552
|
const valueRes = await provider.refreshToken(refreshToken);
|
|
489
|
-
if (valueRes.
|
|
553
|
+
if (!valueRes.ok) {
|
|
490
554
|
setTokenState({ token: null, expiresAt: null, user: void 0, refreshToken: void 0 });
|
|
491
555
|
return err2(valueRes.errors);
|
|
492
556
|
}
|
|
@@ -498,6 +562,7 @@ function isBinaryContentType(contentType) {
|
|
|
498
562
|
if (!accessToken) {
|
|
499
563
|
return err2(AuthErrors.TokenRefreshFailed({ message: "Access token is null after refresh" }));
|
|
500
564
|
}
|
|
565
|
+
sendTokenRefreshed(isProactive ? "proactive" : "expired");
|
|
501
566
|
return ok2(accessToken);
|
|
502
567
|
} finally {
|
|
503
568
|
refreshPromise = null;
|
|
@@ -557,7 +622,7 @@ function isBinaryContentType(contentType) {
|
|
|
557
622
|
}
|
|
558
623
|
if (requiresAuth) {
|
|
559
624
|
const tokenRes = await ensureValidToken();
|
|
560
|
-
if (tokenRes.
|
|
625
|
+
if (!tokenRes.ok) {
|
|
561
626
|
return err2(tokenRes.errors);
|
|
562
627
|
}
|
|
563
628
|
const token = tokenRes.data;
|
|
@@ -656,13 +721,12 @@ function isBinaryContentType(contentType) {
|
|
|
656
721
|
pendingControllers.set(id, controller);
|
|
657
722
|
const merged = { ...options || {}, signal: controller.signal };
|
|
658
723
|
const result = await makeApiRequest(url, merged);
|
|
659
|
-
if (result.
|
|
724
|
+
if (result.ok) {
|
|
660
725
|
sendFetchResult(id, result.data);
|
|
661
726
|
} else {
|
|
662
|
-
const error = result.errors
|
|
727
|
+
const error = result.errors[0];
|
|
663
728
|
const message = error?.message || "Unknown error";
|
|
664
|
-
|
|
665
|
-
sendFetchError(id, message, status);
|
|
729
|
+
sendFetchError(id, message, void 0);
|
|
666
730
|
}
|
|
667
731
|
pendingControllers.delete(id);
|
|
668
732
|
} catch (error) {
|
|
@@ -673,38 +737,40 @@ function isBinaryContentType(contentType) {
|
|
|
673
737
|
}
|
|
674
738
|
case MSG.AUTH_CALL: {
|
|
675
739
|
const { id, payload } = data;
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
740
|
+
await withAuthMutex(async () => {
|
|
741
|
+
try {
|
|
742
|
+
const { method, args, emitEvent } = payload;
|
|
743
|
+
const shouldEmitEvent = emitEvent ?? true;
|
|
744
|
+
if (!provider) {
|
|
745
|
+
sendError(id, err2(InitErrors.NotInitialized()));
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
if (typeof provider[method] !== "function") {
|
|
749
|
+
sendError(id, err2(GeneralErrors.Unexpected({ message: `Method '${method}' not found on provider` })));
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
const result = await provider[method](...args);
|
|
753
|
+
if (!result.ok) {
|
|
754
|
+
sendError(id, result);
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
const tokenInfo = result.data;
|
|
758
|
+
if (!tokenInfo) {
|
|
759
|
+
sendError(id, err2(GeneralErrors.Unexpected({ message: "Provider returned null token info" })));
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
setTokenState(tokenInfo, shouldEmitEvent);
|
|
763
|
+
const now = Date.now();
|
|
764
|
+
const authenticated = accessToken !== null && accessToken !== "" && (expiresAt === null || expiresAt > now);
|
|
765
|
+
sendAuthCallResult(id, {
|
|
766
|
+
authenticated,
|
|
767
|
+
expiresAt,
|
|
768
|
+
user: currentUser
|
|
769
|
+
});
|
|
770
|
+
} catch (error) {
|
|
771
|
+
sendError(id, err2(GeneralErrors.Unexpected({ message: error instanceof Error ? error.message : String(error) })));
|
|
696
772
|
}
|
|
697
|
-
|
|
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
|
-
}
|
|
773
|
+
});
|
|
708
774
|
break;
|
|
709
775
|
}
|
|
710
776
|
case MSG.CANCEL: {
|