fetchguard 1.6.2 → 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 +495 -88
- package/dist/index.js.map +1 -1
- package/dist/worker.js +155 -76
- 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,12 +298,25 @@ 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
|
};
|
|
257
305
|
}
|
|
258
306
|
|
|
307
|
+
// src/provider/parser/normalize.ts
|
|
308
|
+
function normalizeExpiresAt(value) {
|
|
309
|
+
if (value == null) return void 0;
|
|
310
|
+
if (typeof value === "number") {
|
|
311
|
+
return value < 1e12 ? value * 1e3 : value;
|
|
312
|
+
}
|
|
313
|
+
if (typeof value === "string") {
|
|
314
|
+
const ts = Date.parse(value);
|
|
315
|
+
return isNaN(ts) ? void 0 : ts;
|
|
316
|
+
}
|
|
317
|
+
return void 0;
|
|
318
|
+
}
|
|
319
|
+
|
|
259
320
|
// src/provider/parser/body.ts
|
|
260
321
|
var bodyParser = {
|
|
261
322
|
async parse(response) {
|
|
@@ -263,7 +324,7 @@ var bodyParser = {
|
|
|
263
324
|
return {
|
|
264
325
|
token: json.data.accessToken,
|
|
265
326
|
refreshToken: json.data.refreshToken,
|
|
266
|
-
expiresAt: json.data.expiresAt,
|
|
327
|
+
expiresAt: normalizeExpiresAt(json.data.expiresAt),
|
|
267
328
|
user: json.data.user
|
|
268
329
|
};
|
|
269
330
|
}
|
|
@@ -275,7 +336,7 @@ var cookieParser = {
|
|
|
275
336
|
const json = await response.clone().json();
|
|
276
337
|
return {
|
|
277
338
|
token: json.data.accessToken,
|
|
278
|
-
expiresAt: json.data.expiresAt,
|
|
339
|
+
expiresAt: normalizeExpiresAt(json.data.expiresAt),
|
|
279
340
|
user: json.data.user
|
|
280
341
|
};
|
|
281
342
|
}
|
|
@@ -411,8 +472,7 @@ function deserializeFormData(serialized) {
|
|
|
411
472
|
if (typeof value === "string") {
|
|
412
473
|
formData.append(key, value);
|
|
413
474
|
} else {
|
|
414
|
-
const
|
|
415
|
-
const file = new File([uint8Array], value.name, { type: value.type });
|
|
475
|
+
const file = new File([value.buffer], value.name, { type: value.type });
|
|
416
476
|
formData.append(key, file);
|
|
417
477
|
}
|
|
418
478
|
}
|
|
@@ -452,16 +512,33 @@ function isBinaryContentType(contentType) {
|
|
|
452
512
|
let currentUser;
|
|
453
513
|
const pendingControllers = /* @__PURE__ */ new Map();
|
|
454
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
|
+
}
|
|
455
531
|
async function ensureValidToken() {
|
|
456
532
|
if (!provider) {
|
|
457
533
|
return err2(InitErrors.NotInitialized());
|
|
458
534
|
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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);
|
|
465
542
|
}
|
|
466
543
|
if (refreshPromise) {
|
|
467
544
|
const result = await refreshPromise;
|
|
@@ -473,7 +550,7 @@ function isBinaryContentType(contentType) {
|
|
|
473
550
|
return err2(InitErrors.NotInitialized());
|
|
474
551
|
}
|
|
475
552
|
const valueRes = await provider.refreshToken(refreshToken);
|
|
476
|
-
if (valueRes.
|
|
553
|
+
if (!valueRes.ok) {
|
|
477
554
|
setTokenState({ token: null, expiresAt: null, user: void 0, refreshToken: void 0 });
|
|
478
555
|
return err2(valueRes.errors);
|
|
479
556
|
}
|
|
@@ -485,6 +562,7 @@ function isBinaryContentType(contentType) {
|
|
|
485
562
|
if (!accessToken) {
|
|
486
563
|
return err2(AuthErrors.TokenRefreshFailed({ message: "Access token is null after refresh" }));
|
|
487
564
|
}
|
|
565
|
+
sendTokenRefreshed(isProactive ? "proactive" : "expired");
|
|
488
566
|
return ok2(accessToken);
|
|
489
567
|
} finally {
|
|
490
568
|
refreshPromise = null;
|
|
@@ -544,7 +622,7 @@ function isBinaryContentType(contentType) {
|
|
|
544
622
|
}
|
|
545
623
|
if (requiresAuth) {
|
|
546
624
|
const tokenRes = await ensureValidToken();
|
|
547
|
-
if (tokenRes.
|
|
625
|
+
if (!tokenRes.ok) {
|
|
548
626
|
return err2(tokenRes.errors);
|
|
549
627
|
}
|
|
550
628
|
const token = tokenRes.data;
|
|
@@ -643,13 +721,12 @@ function isBinaryContentType(contentType) {
|
|
|
643
721
|
pendingControllers.set(id, controller);
|
|
644
722
|
const merged = { ...options || {}, signal: controller.signal };
|
|
645
723
|
const result = await makeApiRequest(url, merged);
|
|
646
|
-
if (result.
|
|
724
|
+
if (result.ok) {
|
|
647
725
|
sendFetchResult(id, result.data);
|
|
648
726
|
} else {
|
|
649
|
-
const error = result.errors
|
|
727
|
+
const error = result.errors[0];
|
|
650
728
|
const message = error?.message || "Unknown error";
|
|
651
|
-
|
|
652
|
-
sendFetchError(id, message, status);
|
|
729
|
+
sendFetchError(id, message, void 0);
|
|
653
730
|
}
|
|
654
731
|
pendingControllers.delete(id);
|
|
655
732
|
} catch (error) {
|
|
@@ -660,38 +737,40 @@ function isBinaryContentType(contentType) {
|
|
|
660
737
|
}
|
|
661
738
|
case MSG.AUTH_CALL: {
|
|
662
739
|
const { id, payload } = data;
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
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) })));
|
|
669
772
|
}
|
|
670
|
-
|
|
671
|
-
sendError(id, err2(GeneralErrors.Unexpected({ message: `Method '${method}' not found on provider` })));
|
|
672
|
-
break;
|
|
673
|
-
}
|
|
674
|
-
const result = await provider[method](...args);
|
|
675
|
-
if (result.isError()) {
|
|
676
|
-
sendError(id, result);
|
|
677
|
-
break;
|
|
678
|
-
}
|
|
679
|
-
const tokenInfo = result.data;
|
|
680
|
-
if (!tokenInfo) {
|
|
681
|
-
sendError(id, err2(GeneralErrors.Unexpected({ message: "Provider returned null token info" })));
|
|
682
|
-
break;
|
|
683
|
-
}
|
|
684
|
-
setTokenState(tokenInfo, shouldEmitEvent);
|
|
685
|
-
const now = Date.now();
|
|
686
|
-
const authenticated = accessToken !== null && accessToken !== "" && (expiresAt === null || expiresAt > now);
|
|
687
|
-
sendAuthCallResult(id, {
|
|
688
|
-
authenticated,
|
|
689
|
-
expiresAt,
|
|
690
|
-
user: currentUser
|
|
691
|
-
});
|
|
692
|
-
} catch (error) {
|
|
693
|
-
sendError(id, err2(GeneralErrors.Unexpected({ message: error instanceof Error ? error.message : String(error) })));
|
|
694
|
-
}
|
|
773
|
+
});
|
|
695
774
|
break;
|
|
696
775
|
}
|
|
697
776
|
case MSG.CANCEL: {
|