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/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,12 +298,25 @@ 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
  };
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 uint8Array = new Uint8Array(value.data);
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
- if (accessToken && expiresAt) {
460
- const refreshEarlyMs = config?.refreshEarlyMs ?? DEFAULT_REFRESH_EARLY_MS;
461
- const timeLeft = expiresAt - Date.now();
462
- if (timeLeft > refreshEarlyMs) {
463
- return ok2(accessToken);
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.isError()) {
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.isError()) {
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.isOkWithData()) {
724
+ if (result.ok) {
647
725
  sendFetchResult(id, result.data);
648
726
  } else {
649
- const error = result.errors?.[0];
727
+ const error = result.errors[0];
650
728
  const message = error?.message || "Unknown error";
651
- const status = result.status;
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
- try {
664
- const { method, args, emitEvent } = payload;
665
- const shouldEmitEvent = emitEvent ?? true;
666
- if (!provider) {
667
- sendError(id, err2(InitErrors.NotInitialized()));
668
- 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) })));
669
772
  }
670
- if (typeof provider[method] !== "function") {
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: {