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/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("UNEXPECTED", "Unexpected error", 500),
31
- UnknownMessage: defineError("UNKNOWN_MESSAGE", "Unknown message type", 400),
32
- ResultParse: defineError("RESULT_PARSE_ERROR", "Failed to parse result", 500)
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("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)
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("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)
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("DOMAIN_NOT_ALLOWED", "Domain not allowed: {url}", 403)
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("NETWORK_ERROR", "Network error", 500),
51
- Cancelled: defineError("REQUEST_CANCELLED", "Request was cancelled", 499),
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("HTTP_ERROR", "HTTP {status} error", 500),
84
+ HttpError: defineErrorAdvanced(ERROR_CODES.HTTP_ERROR, "HTTP {status} error"),
54
85
  // Response parsing errors
55
- ResponseParseFailed: defineError("RESPONSE_PARSE_FAILED", "Failed to parse response body", 500)
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
- post({
65
- type: MSG.ERROR,
66
- id,
67
- payload: { errors: result.errors, meta: result.meta, status: result.status }
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, response) {
107
+ function sendFetchResult(id, envelope) {
71
108
  post({
72
109
  type: MSG.FETCH_RESULT,
73
110
  id,
74
- payload: response
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 }, response.status);
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 }, response.status);
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 }, response.status);
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(dbName = "FetchGuardDB", refreshTokenKey = "refreshToken") {
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
- console.warn("Failed to get refresh token from IndexedDB:", error);
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
- console.warn("Failed to save refresh token to IndexedDB:", error);
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 uint8Array = new Uint8Array(value.data);
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
- 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
- }
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.isError()) {
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.isError()) {
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.isOkWithData()) {
757
+ if (result.ok) {
660
758
  sendFetchResult(id, result.data);
661
759
  } else {
662
- const error = result.errors?.[0];
760
+ const error = result.errors[0];
663
761
  const message = error?.message || "Unknown error";
664
- const status = result.status;
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
- 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;
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
- const tokenInfo = result.data;
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: {