fetchguard 1.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 ADDED
@@ -0,0 +1,587 @@
1
+ // src/worker.ts
2
+ import { ok as ok2, err as err2 } from "ts-micro-result";
3
+
4
+ // src/messages.ts
5
+ var MSG = Object.freeze({
6
+ ...Object.fromEntries(
7
+ Object.keys({}).map((k) => [k, k])
8
+ ),
9
+ ...Object.fromEntries(
10
+ Object.keys({}).map((k) => [k, k])
11
+ )
12
+ });
13
+
14
+ // src/constants.ts
15
+ var DEFAULT_REFRESH_EARLY_MS = 6e4;
16
+
17
+ // src/errors.ts
18
+ import { defineError, defineErrorAdvanced } from "ts-micro-result";
19
+ var GeneralErrors = {
20
+ Unexpected: defineError("UNEXPECTED", "Unexpected error", 500),
21
+ UnknownMessage: defineError("UNKNOWN_MESSAGE", "Unknown message type", 400),
22
+ ResultParse: defineError("RESULT_PARSE_ERROR", "Failed to parse result", 500)
23
+ };
24
+ var InitErrors = {
25
+ NotInitialized: defineError("INIT_ERROR", "Worker not initialized", 500),
26
+ ProviderInitFailed: defineError("PROVIDER_INIT_FAILED", "Failed to initialize provider", 500),
27
+ InitFailed: defineError("INIT_FAILED", "Initialization failed", 500)
28
+ };
29
+ var AuthErrors = {
30
+ TokenRefreshFailed: defineError("TOKEN_REFRESH_FAILED", "Token refresh failed", 401),
31
+ LoginFailed: defineError("LOGIN_FAILED", "Login failed", 401),
32
+ LogoutFailed: defineError("LOGOUT_FAILED", "Logout failed", 500),
33
+ NotAuthenticated: defineError("NOT_AUTHENTICATED", "User is not authenticated", 401)
34
+ };
35
+ var DomainErrors = {
36
+ NotAllowed: defineErrorAdvanced("DOMAIN_NOT_ALLOWED", "Domain not allowed: {url}", 403)
37
+ };
38
+ var NetworkErrors = {
39
+ NetworkError: defineError("NETWORK_ERROR", "Network error", 500),
40
+ HttpError: defineError("HTTP_ERROR", "HTTP error", 500),
41
+ FetchError: defineError("FETCH_ERROR", "Fetch error", 500)
42
+ };
43
+ var RequestErrors = {
44
+ Cancelled: defineError("REQUEST_CANCELLED", "Request was cancelled", 499),
45
+ Timeout: defineError("REQUEST_TIMEOUT", "Request timeout", 408)
46
+ };
47
+
48
+ // src/worker-post.ts
49
+ function post(message) {
50
+ ;
51
+ self.postMessage(message);
52
+ }
53
+ function sendResult(id, result) {
54
+ post({
55
+ type: MSG.RESULT,
56
+ id,
57
+ payload: { result: result.toJSON() }
58
+ });
59
+ }
60
+ function sendFetchResult(id, status, body, headers) {
61
+ post({
62
+ type: MSG.FETCH_RESULT,
63
+ id,
64
+ payload: headers ? { status, body, headers } : { status, body }
65
+ });
66
+ }
67
+ function sendFetchError(id, error, status) {
68
+ post({
69
+ type: MSG.FETCH_ERROR,
70
+ id,
71
+ payload: { error, status }
72
+ });
73
+ }
74
+ function sendReady() {
75
+ post({
76
+ type: MSG.READY,
77
+ id: `evt_${Date.now()}`
78
+ });
79
+ }
80
+ function sendPong(id, timestamp) {
81
+ post({
82
+ type: MSG.PONG,
83
+ id,
84
+ payload: { timestamp }
85
+ });
86
+ }
87
+ function sendAuthStateChanged(authenticated, expiresAt, user) {
88
+ post({
89
+ type: MSG.AUTH_STATE_CHANGED,
90
+ id: `evt_${Date.now()}`,
91
+ payload: { authenticated, expiresAt, user }
92
+ });
93
+ }
94
+
95
+ // src/utils/registry.ts
96
+ var registry = /* @__PURE__ */ new Map();
97
+ function getProvider(name) {
98
+ const provider = registry.get(name);
99
+ if (!provider) {
100
+ throw new Error(`Provider '${name}' not found. Available providers: ${Array.from(registry.keys()).join(", ")}`);
101
+ }
102
+ return provider;
103
+ }
104
+
105
+ // src/provider/create-provider.ts
106
+ import { ok, err } from "ts-micro-result";
107
+ function createProvider(config) {
108
+ const baseProvider = {
109
+ async refreshToken(refreshToken) {
110
+ let currentRefreshToken = refreshToken;
111
+ if (currentRefreshToken === null && config.refreshStorage) {
112
+ currentRefreshToken = await config.refreshStorage.get();
113
+ }
114
+ try {
115
+ const response = await config.strategy.refresh(currentRefreshToken);
116
+ if (!response.ok) {
117
+ return err(AuthErrors.TokenRefreshFailed({ message: `HTTP ${response.status}` }));
118
+ }
119
+ const tokenInfo = await config.parser.parse(response);
120
+ if (!tokenInfo.token) {
121
+ return err(AuthErrors.TokenRefreshFailed({ message: "No access token in response" }));
122
+ }
123
+ if (config.refreshStorage && tokenInfo.refreshToken) {
124
+ await config.refreshStorage.set(tokenInfo.refreshToken);
125
+ }
126
+ return ok(tokenInfo);
127
+ } catch (error) {
128
+ return err(NetworkErrors.NetworkError({ message: String(error) }));
129
+ }
130
+ },
131
+ async login(payload) {
132
+ try {
133
+ const response = await config.strategy.login(payload);
134
+ if (!response.ok) {
135
+ return err(AuthErrors.LoginFailed({ message: `HTTP ${response.status}` }));
136
+ }
137
+ const tokenInfo = await config.parser.parse(response);
138
+ if (!tokenInfo.token) {
139
+ return err(AuthErrors.LoginFailed({ message: "No access token in response" }));
140
+ }
141
+ if (config.refreshStorage && tokenInfo.refreshToken) {
142
+ await config.refreshStorage.set(tokenInfo.refreshToken);
143
+ }
144
+ return ok(tokenInfo);
145
+ } catch (error) {
146
+ return err(NetworkErrors.NetworkError({ message: String(error) }));
147
+ }
148
+ },
149
+ async logout(payload) {
150
+ try {
151
+ const response = await config.strategy.logout(payload);
152
+ if (!response.ok) {
153
+ return err(AuthErrors.LogoutFailed({ message: `HTTP ${response.status}` }));
154
+ }
155
+ if (config.refreshStorage) {
156
+ await config.refreshStorage.set(null);
157
+ }
158
+ return ok({
159
+ token: "",
160
+ refreshToken: void 0,
161
+ expiresAt: void 0,
162
+ user: void 0
163
+ });
164
+ } catch (error) {
165
+ return err(NetworkErrors.NetworkError({ message: String(error) }));
166
+ }
167
+ }
168
+ };
169
+ if (config.customMethods) {
170
+ return {
171
+ ...baseProvider,
172
+ ...config.customMethods
173
+ };
174
+ }
175
+ return baseProvider;
176
+ }
177
+
178
+ // src/provider/storage/indexeddb.ts
179
+ function createIndexedDBStorage(dbName = "FetchGuardDB", refreshTokenKey = "refreshToken") {
180
+ const storeName = "tokens";
181
+ const openDB = () => {
182
+ return new Promise((resolve, reject) => {
183
+ const request = indexedDB.open(dbName, 1);
184
+ request.onerror = () => reject(request.error);
185
+ request.onsuccess = () => resolve(request.result);
186
+ request.onupgradeneeded = (event) => {
187
+ const db = event.target.result;
188
+ if (!db.objectStoreNames.contains(storeName)) {
189
+ const store = db.createObjectStore(storeName, { keyPath: "key" });
190
+ store.createIndex("timestamp", "timestamp", { unique: false });
191
+ }
192
+ };
193
+ });
194
+ };
195
+ const promisifyRequest = (request) => {
196
+ return new Promise((resolve, reject) => {
197
+ request.onsuccess = () => resolve(request.result);
198
+ request.onerror = () => reject(request.error);
199
+ });
200
+ };
201
+ return {
202
+ async get() {
203
+ try {
204
+ const db = await openDB();
205
+ const transaction = db.transaction([storeName], "readonly");
206
+ const store = transaction.objectStore(storeName);
207
+ const result = await promisifyRequest(store.get(refreshTokenKey));
208
+ return result?.value || null;
209
+ } catch (error) {
210
+ console.warn("Failed to get refresh token from IndexedDB:", error);
211
+ return null;
212
+ }
213
+ },
214
+ async set(token) {
215
+ try {
216
+ const db = await openDB();
217
+ const transaction = db.transaction([storeName], "readwrite");
218
+ const store = transaction.objectStore(storeName);
219
+ if (token) {
220
+ await promisifyRequest(store.put({ key: refreshTokenKey, value: token, timestamp: Date.now() }));
221
+ } else {
222
+ await promisifyRequest(store.delete(refreshTokenKey));
223
+ }
224
+ } catch (error) {
225
+ console.warn("Failed to save refresh token to IndexedDB:", error);
226
+ }
227
+ }
228
+ };
229
+ }
230
+
231
+ // src/provider/parser/body.ts
232
+ var bodyParser = {
233
+ async parse(response) {
234
+ const json = await response.clone().json();
235
+ return {
236
+ token: json.data.accessToken,
237
+ refreshToken: json.data.refreshToken,
238
+ expiresAt: json.data.expiresAt,
239
+ user: json.data.user
240
+ };
241
+ }
242
+ };
243
+
244
+ // src/provider/parser/cookie.ts
245
+ var cookieParser = {
246
+ async parse(response) {
247
+ const json = await response.clone().json();
248
+ return {
249
+ token: json.data.accessToken,
250
+ expiresAt: json.data.expiresAt,
251
+ user: json.data.user
252
+ };
253
+ }
254
+ };
255
+
256
+ // src/provider/strategy/cookie.ts
257
+ function createCookieStrategy(config) {
258
+ return {
259
+ async refresh() {
260
+ return fetch(config.refreshUrl, {
261
+ method: "POST",
262
+ headers: { "Content-Type": "application/json" },
263
+ credentials: "include"
264
+ });
265
+ },
266
+ async login(payload) {
267
+ return fetch(config.loginUrl, {
268
+ method: "POST",
269
+ headers: { "Content-Type": "application/json" },
270
+ body: JSON.stringify(payload),
271
+ credentials: "include"
272
+ });
273
+ },
274
+ async logout(payload) {
275
+ return fetch(config.logoutUrl, {
276
+ method: "POST",
277
+ headers: { "Content-Type": "application/json" },
278
+ body: payload ? JSON.stringify(payload) : void 0,
279
+ credentials: "include"
280
+ });
281
+ }
282
+ };
283
+ }
284
+ var cookieStrategy = createCookieStrategy({
285
+ refreshUrl: "/auth/refresh",
286
+ loginUrl: "/auth/login",
287
+ logoutUrl: "/auth/logout"
288
+ });
289
+
290
+ // src/provider/strategy/body.ts
291
+ function createBodyStrategy(config) {
292
+ return {
293
+ async refresh(refreshToken) {
294
+ if (!refreshToken) {
295
+ throw new Error("No refresh token available");
296
+ }
297
+ return fetch(config.refreshUrl, {
298
+ method: "POST",
299
+ headers: { "Content-Type": "application/json" },
300
+ body: JSON.stringify({ refreshToken }),
301
+ credentials: "include"
302
+ });
303
+ },
304
+ async login(payload) {
305
+ return fetch(config.loginUrl, {
306
+ method: "POST",
307
+ headers: { "Content-Type": "application/json" },
308
+ body: JSON.stringify(payload),
309
+ credentials: "include"
310
+ });
311
+ },
312
+ async logout(payload) {
313
+ return fetch(config.logoutUrl, {
314
+ method: "POST",
315
+ headers: { "Content-Type": "application/json" },
316
+ body: payload ? JSON.stringify(payload) : void 0,
317
+ credentials: "include"
318
+ });
319
+ }
320
+ };
321
+ }
322
+ var bodyStrategy = createBodyStrategy({
323
+ refreshUrl: "/auth/refresh",
324
+ loginUrl: "/auth/login",
325
+ logoutUrl: "/auth/logout"
326
+ });
327
+
328
+ // src/provider/presets.ts
329
+ function createCookieProvider(config) {
330
+ return createProvider({
331
+ refreshStorage: void 0,
332
+ parser: cookieParser,
333
+ strategy: createCookieStrategy(config)
334
+ });
335
+ }
336
+ function createBodyProvider(config) {
337
+ return createProvider({
338
+ refreshStorage: createIndexedDBStorage("FetchGuardDB", config.refreshTokenKey || "refreshToken"),
339
+ parser: bodyParser,
340
+ strategy: createBodyStrategy(config)
341
+ });
342
+ }
343
+
344
+ // src/provider/register-presets.ts
345
+ function buildProviderFromPreset(config) {
346
+ switch (config.type) {
347
+ case "cookie-auth":
348
+ return createCookieProvider({
349
+ refreshUrl: config.refreshUrl,
350
+ loginUrl: config.loginUrl,
351
+ logoutUrl: config.logoutUrl
352
+ });
353
+ case "body-auth":
354
+ return createBodyProvider({
355
+ refreshUrl: config.refreshUrl,
356
+ loginUrl: config.loginUrl,
357
+ logoutUrl: config.logoutUrl,
358
+ refreshTokenKey: config.refreshTokenKey
359
+ });
360
+ default:
361
+ throw new Error(`Unknown provider type: ${config.type}`);
362
+ }
363
+ }
364
+
365
+ // src/worker.ts
366
+ (function() {
367
+ let config = null;
368
+ let provider = null;
369
+ let accessToken = null;
370
+ let refreshToken = null;
371
+ let expiresAt = null;
372
+ let currentUser;
373
+ const pendingControllers = /* @__PURE__ */ new Map();
374
+ let refreshPromise = null;
375
+ async function ensureValidToken() {
376
+ if (accessToken && expiresAt) {
377
+ const refreshEarlyMs = config?.refreshEarlyMs ?? DEFAULT_REFRESH_EARLY_MS;
378
+ const timeLeft = expiresAt - Date.now();
379
+ if (timeLeft > refreshEarlyMs) {
380
+ return ok2(accessToken);
381
+ }
382
+ }
383
+ if (refreshPromise) {
384
+ await refreshPromise;
385
+ return ok2(accessToken);
386
+ }
387
+ refreshPromise = (async () => {
388
+ try {
389
+ const valueRes = await provider.refreshToken(refreshToken);
390
+ if (valueRes.isError()) {
391
+ setTokenState({ token: null, expiresAt: null, user: void 0, refreshToken: void 0 });
392
+ return;
393
+ }
394
+ const tokenInfo = valueRes.data;
395
+ setTokenState(tokenInfo);
396
+ } finally {
397
+ refreshPromise = null;
398
+ }
399
+ })();
400
+ await refreshPromise;
401
+ return ok2(accessToken);
402
+ }
403
+ function validateDomain(url) {
404
+ if (!config?.allowedDomains?.length) {
405
+ return true;
406
+ }
407
+ try {
408
+ const urlObj = new URL(url);
409
+ const hostname = urlObj.hostname;
410
+ const port = urlObj.port;
411
+ for (const entry of config.allowedDomains) {
412
+ const idx = entry.lastIndexOf(":");
413
+ const hasPort = idx > -1 && entry.indexOf(":") === idx;
414
+ const pattern = hasPort ? entry.slice(0, idx) : entry;
415
+ const entryPort = hasPort ? entry.slice(idx + 1) : "";
416
+ const isWildcard = pattern.startsWith("*.");
417
+ const base = isWildcard ? pattern.slice(2) : pattern;
418
+ const hostnameMatch = isWildcard ? hostname === base || hostname.endsWith("." + base) : hostname === base;
419
+ if (!hostnameMatch) continue;
420
+ if (hasPort) {
421
+ if (port === entryPort) return true;
422
+ continue;
423
+ }
424
+ return true;
425
+ }
426
+ return false;
427
+ } catch {
428
+ return false;
429
+ }
430
+ }
431
+ async function makeApiRequest(url, options = {}) {
432
+ if (!config) {
433
+ return err2(InitErrors.NotInitialized());
434
+ }
435
+ if (!validateDomain(url)) {
436
+ return err2(DomainErrors.NotAllowed({ url }));
437
+ }
438
+ const requiresAuth = options.requiresAuth !== false;
439
+ const includeHeaders = options.includeHeaders === true;
440
+ const fetchOptions = { ...options };
441
+ delete fetchOptions.requiresAuth;
442
+ delete fetchOptions.includeHeaders;
443
+ const headers = {
444
+ ...fetchOptions.headers || {}
445
+ };
446
+ if (!headers["Content-Type"] && !headers["content-type"] && fetchOptions.body) {
447
+ if (typeof fetchOptions.body === "object" && !(fetchOptions.body instanceof FormData) && !(fetchOptions.body instanceof URLSearchParams)) {
448
+ headers["Content-Type"] = "application/json";
449
+ }
450
+ }
451
+ if (requiresAuth) {
452
+ const tokenRes = await ensureValidToken();
453
+ if (tokenRes.isError()) return tokenRes;
454
+ const token = tokenRes.data;
455
+ if (token) {
456
+ headers["Authorization"] = `Bearer ${token}`;
457
+ }
458
+ }
459
+ let response = null;
460
+ let networkErr = null;
461
+ response = await fetch(url, { ...fetchOptions, headers, credentials: "include" }).catch((e) => {
462
+ const aborted = e && e.name === "AbortError";
463
+ networkErr = aborted ? err2(RequestErrors.Cancelled()) : err2(NetworkErrors.NetworkError({ message: String(e) }));
464
+ return null;
465
+ });
466
+ if (!response) return networkErr ?? err2(NetworkErrors.NetworkError({ message: "Unknown network error" }));
467
+ const body = await response.text();
468
+ let responseHeaders;
469
+ if (includeHeaders) {
470
+ responseHeaders = {};
471
+ response.headers.forEach((value, key) => {
472
+ responseHeaders[key] = value;
473
+ });
474
+ }
475
+ return response.ok ? ok2({ body, status: response.status, headers: responseHeaders }) : err2(NetworkErrors.HttpError({ message: `HTTP ${response.status}: ${body}` }));
476
+ }
477
+ function setTokenState(tokenInfo) {
478
+ accessToken = tokenInfo.token;
479
+ expiresAt = tokenInfo.expiresAt ?? null;
480
+ currentUser = tokenInfo.user;
481
+ refreshToken = tokenInfo.refreshToken ?? null;
482
+ postAuthChanged();
483
+ }
484
+ function postAuthChanged() {
485
+ const now = Date.now();
486
+ const authenticated = accessToken !== null && accessToken !== "" && (expiresAt === null || expiresAt > now);
487
+ sendAuthStateChanged(authenticated, expiresAt, currentUser);
488
+ }
489
+ self.onmessage = async (event) => {
490
+ const data = event.data;
491
+ switch (data.type) {
492
+ case MSG.SETUP: {
493
+ try {
494
+ const payload = data.payload;
495
+ config = payload.config;
496
+ const providerConfig = payload.providerConfig;
497
+ if (typeof providerConfig === "string") {
498
+ provider = getProvider(providerConfig);
499
+ } else if (providerConfig && typeof providerConfig === "object" && "type" in providerConfig) {
500
+ provider = buildProviderFromPreset(providerConfig);
501
+ } else {
502
+ throw new Error("Invalid provider config");
503
+ }
504
+ sendReady();
505
+ } catch (error) {
506
+ console.error("[FetchGuard Worker] Setup failed:", error);
507
+ }
508
+ break;
509
+ }
510
+ case MSG.FETCH: {
511
+ const { id } = data;
512
+ try {
513
+ const { url, options } = data.payload;
514
+ const controller = new AbortController();
515
+ pendingControllers.set(id, controller);
516
+ const merged = { ...options || {}, signal: controller.signal };
517
+ const result = await makeApiRequest(url, merged);
518
+ if (result.isOk()) {
519
+ const response = result.data;
520
+ sendFetchResult(id, response.status, response.body, response.headers);
521
+ } else {
522
+ const error = result.errors?.[0];
523
+ const message = error?.message || "Unknown error";
524
+ const status = result.status;
525
+ sendFetchError(id, message, status);
526
+ }
527
+ pendingControllers.delete(id);
528
+ } catch (error) {
529
+ pendingControllers.delete(id);
530
+ sendFetchError(id, error instanceof Error ? error.message : String(error), void 0);
531
+ }
532
+ break;
533
+ }
534
+ case MSG.AUTH_CALL: {
535
+ const { id, payload } = data;
536
+ try {
537
+ const { method, args } = payload;
538
+ if (typeof provider[method] !== "function") {
539
+ sendResult(id, err2(GeneralErrors.Unexpected({ message: `Method '${method}' not found on provider` })));
540
+ break;
541
+ }
542
+ const result = await provider[method](...args);
543
+ if (result.isError()) {
544
+ sendResult(id, result);
545
+ break;
546
+ }
547
+ const tokenInfo = result.data;
548
+ setTokenState(tokenInfo);
549
+ sendResult(id, ok2(void 0));
550
+ } catch (error) {
551
+ sendResult(id, err2(GeneralErrors.Unexpected({ message: error instanceof Error ? error.message : String(error) })));
552
+ }
553
+ break;
554
+ }
555
+ case MSG.CANCEL: {
556
+ try {
557
+ const { id } = data;
558
+ const controller = pendingControllers.get(id);
559
+ if (controller) {
560
+ controller.abort();
561
+ pendingControllers.delete(id);
562
+ }
563
+ } catch (error) {
564
+ if (config?.debug) {
565
+ console.error("CANCEL error:", error);
566
+ }
567
+ }
568
+ break;
569
+ }
570
+ case MSG.PING: {
571
+ const { id } = data;
572
+ try {
573
+ const ts = data.payload?.timestamp ?? Date.now();
574
+ sendPong(id, ts);
575
+ } catch (error) {
576
+ sendResult(id, err2(GeneralErrors.Unexpected({ message: error instanceof Error ? error.message : String(error) })));
577
+ }
578
+ break;
579
+ }
580
+ default: {
581
+ const anyData = data;
582
+ sendResult(anyData.id, err2(GeneralErrors.UnknownMessage({ message: `Unknown message type: ${String(anyData.type)}` })));
583
+ }
584
+ }
585
+ };
586
+ })();
587
+ //# sourceMappingURL=worker.js.map