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/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("UNEXPECTED", "Unexpected error", 500),
31
- UnknownMessage: defineError("UNKNOWN_MESSAGE", "Unknown message type", 400),
32
- ResultParse: defineError("RESULT_PARSE_ERROR", "Failed to parse result", 500)
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("INIT_ERROR", "Worker not initialized", 500),
36
- ProviderInitFailed: defineError("PROVIDER_INIT_FAILED", "Failed to initialize provider", 500),
37
- InitFailed: defineError("INIT_FAILED", "Initialization failed", 500)
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("TOKEN_REFRESH_FAILED", "Token refresh failed", 401),
41
- LoginFailed: defineError("LOGIN_FAILED", "Login failed", 401),
42
- LogoutFailed: defineError("LOGOUT_FAILED", "Logout failed", 500),
43
- NotAuthenticated: defineError("NOT_AUTHENTICATED", "User is not authenticated", 401)
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("DOMAIN_NOT_ALLOWED", "Domain not allowed: {url}", 403)
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("NETWORK_ERROR", "Network error", 500),
51
- Cancelled: defineError("REQUEST_CANCELLED", "Request was cancelled", 499),
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("HTTP_ERROR", "HTTP {status} error", 500),
82
+ HttpError: defineErrorAdvanced(ERROR_CODES.HTTP_ERROR, "HTTP {status} error"),
54
83
  // Response parsing errors
55
- ResponseParseFailed: defineError("RESPONSE_PARSE_FAILED", "Failed to parse response body", 500)
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
- post({
65
- type: MSG.ERROR,
66
- id,
67
- payload: { errors: result.errors, meta: result.meta, status: result.status }
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, response) {
105
+ function sendFetchResult(id, envelope) {
71
106
  post({
72
107
  type: MSG.FETCH_RESULT,
73
108
  id,
74
- payload: response
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 }, response.status);
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 }, response.status);
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 }, response.status);
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(dbName = "FetchGuardDB", refreshTokenKey = "refreshToken") {
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
- console.warn("Failed to get refresh token from IndexedDB:", error);
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
- console.warn("Failed to save refresh token to IndexedDB:", error);
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 uint8Array = new Uint8Array(value.data);
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
- if (accessToken && expiresAt) {
473
- const refreshEarlyMs = config?.refreshEarlyMs ?? DEFAULT_REFRESH_EARLY_MS;
474
- const timeLeft = expiresAt - Date.now();
475
- if (timeLeft > refreshEarlyMs) {
476
- return ok2(accessToken);
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.isError()) {
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.isError()) {
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.isOkWithData()) {
724
+ if (result.ok) {
660
725
  sendFetchResult(id, result.data);
661
726
  } else {
662
- const error = result.errors?.[0];
727
+ const error = result.errors[0];
663
728
  const message = error?.message || "Unknown error";
664
- const status = result.status;
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
- try {
677
- const { method, args, emitEvent } = payload;
678
- const shouldEmitEvent = emitEvent ?? true;
679
- if (!provider) {
680
- sendError(id, err2(InitErrors.NotInitialized()));
681
- break;
682
- }
683
- if (typeof provider[method] !== "function") {
684
- sendError(id, err2(GeneralErrors.Unexpected({ message: `Method '${method}' not found on provider` })));
685
- break;
686
- }
687
- const result = await provider[method](...args);
688
- if (result.isError()) {
689
- sendError(id, result);
690
- break;
691
- }
692
- const tokenInfo = result.data;
693
- if (!tokenInfo) {
694
- sendError(id, err2(GeneralErrors.Unexpected({ message: "Provider returned null token info" })));
695
- break;
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
- 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
- }
773
+ });
708
774
  break;
709
775
  }
710
776
  case MSG.CANCEL: {