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/README.md +235 -65
- package/dist/index.d.ts +540 -31
- package/dist/index.js +480 -86
- package/dist/index.js.map +1 -1
- package/dist/worker.js +140 -74
- package/dist/worker.js.map +1 -1
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
# FetchGuard
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/fetchguard)
|
|
4
|
-

|
|
5
4
|
[](https://www.npmjs.com/package/fetchguard)
|
|
6
5
|
[](https://github.com/minhtaimc/fetchguard/blob/main/LICENSE)
|
|
7
6
|
|
|
8
7
|
|
|
9
8
|
FetchGuard is a secure, type-safe API client that runs your network requests inside a Web Worker. Access tokens never touch the main thread, reducing XSS risk while providing automatic token refresh, domain allow-listing, and a clean Result-based API.
|
|
10
9
|
|
|
10
|
+
> **FetchGuard is a transport & security gateway, not an API client nor a business SDK.**
|
|
11
|
+
|
|
12
|
+
|
|
11
13
|
## Why FetchGuard
|
|
12
14
|
|
|
13
15
|
- XSS hardening via closure: tokens live inside a Worker IIFE closure, not in `window`, not in JS-readable cookies, and not in local/session storage.
|
|
@@ -18,6 +20,33 @@ FetchGuard is a secure, type-safe API client that runs your network requests ins
|
|
|
18
20
|
- Domain allow-list: block requests to unexpected hosts (wildcards and ports supported).
|
|
19
21
|
- FormData support: automatic serialization for file uploads through the Worker.
|
|
20
22
|
|
|
23
|
+
|
|
24
|
+
## Comparison with Alternatives
|
|
25
|
+
|
|
26
|
+
| Feature | FetchGuard | axios | ky | ofetch |
|
|
27
|
+
|---------|-----------|-------|-----|--------|
|
|
28
|
+
| XSS Token Protection | Worker + IIFE closure | None | None | None |
|
|
29
|
+
| Result Pattern | `ts-micro-result` | throw | throw | throw |
|
|
30
|
+
| Auto Token Refresh | Proactive (before expiry) | Manual | Manual | Manual |
|
|
31
|
+
| Domain Whitelist | Built-in | None | None | None |
|
|
32
|
+
| Bundle Size | ~10KB | ~30KB | ~8KB | ~5KB |
|
|
33
|
+
|
|
34
|
+
**Key differentiator:** FetchGuard is the only library that isolates tokens in a Web Worker IIFE closure, making them inaccessible to XSS attacks on the main thread.
|
|
35
|
+
|
|
36
|
+
## When to Use FetchGuard
|
|
37
|
+
|
|
38
|
+
**Good fit:**
|
|
39
|
+
- SPAs that need to protect tokens from XSS attacks
|
|
40
|
+
- Apps requiring automatic token refresh without UI flicker
|
|
41
|
+
- Projects that prefer Result-based error handling over try/catch
|
|
42
|
+
- Teams wanting domain-level request restrictions
|
|
43
|
+
|
|
44
|
+
**Not ideal for:**
|
|
45
|
+
- Server-side rendering (Web Workers don't run on the server)
|
|
46
|
+
- High-throughput scenarios requiring many concurrent requests
|
|
47
|
+
- Apps needing streaming responses (SSE/WebSocket)
|
|
48
|
+
- Legacy browser support (requires Web Worker)
|
|
49
|
+
|
|
21
50
|
## Architecture (Simplified)
|
|
22
51
|
|
|
23
52
|
```
|
|
@@ -86,6 +115,61 @@ Hardening tips:
|
|
|
86
115
|
- Use the domain allow-list aggressively (include explicit ports in development).
|
|
87
116
|
- Avoid logging or exposing tokens in any responses; keep login/refresh parsing inside the worker.
|
|
88
117
|
|
|
118
|
+
## Multi-Tab Refresh Behavior
|
|
119
|
+
|
|
120
|
+
When using cookie-based auth with multiple browser tabs, each tab has its own FetchGuard Worker instance but they share the same refresh token cookie. This means concurrent refresh requests are legitimate and expected.
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
Tab 1: Token expires → calls /refresh
|
|
124
|
+
Tab 2: Token expires → calls /refresh (same cookie)
|
|
125
|
+
Tab 3: Token expires → calls /refresh (same cookie)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**Server Requirements:**
|
|
129
|
+
|
|
130
|
+
Your auth server MUST handle concurrent refresh requests gracefully. Recommended patterns:
|
|
131
|
+
|
|
132
|
+
1. **Grace Window (Recommended)**
|
|
133
|
+
Allow the old refresh token for N seconds after rotation:
|
|
134
|
+
```typescript
|
|
135
|
+
// Server-side example
|
|
136
|
+
const GRACE_WINDOW_MS = 30_000 // 30 seconds
|
|
137
|
+
|
|
138
|
+
async function refreshToken(oldToken: string) {
|
|
139
|
+
const record = await db.findRefreshToken(oldToken)
|
|
140
|
+
|
|
141
|
+
if (record.rotatedAt) {
|
|
142
|
+
// Token was already rotated
|
|
143
|
+
const elapsed = Date.now() - record.rotatedAt
|
|
144
|
+
if (elapsed < GRACE_WINDOW_MS) {
|
|
145
|
+
// Within grace window: return same new tokens
|
|
146
|
+
return record.replacementTokens
|
|
147
|
+
}
|
|
148
|
+
throw new Error('Refresh token expired')
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// First use: rotate and mark
|
|
152
|
+
const newTokens = generateNewTokens()
|
|
153
|
+
await db.markRotated(oldToken, newTokens, Date.now())
|
|
154
|
+
return newTokens
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
2. **Token Family Tracking**
|
|
159
|
+
Track token lineage and allow previous tokens within grace period.
|
|
160
|
+
|
|
161
|
+
3. **Idempotency Keys**
|
|
162
|
+
Accept a unique key per refresh attempt and dedupe server-side.
|
|
163
|
+
|
|
164
|
+
**FetchGuard Grace Window:**
|
|
165
|
+
|
|
166
|
+
FetchGuard uses proactive token refresh (default: 60 seconds before expiry via `refreshEarlyMs`). This means:
|
|
167
|
+
- Tokens are refreshed BEFORE they expire, not after
|
|
168
|
+
- Multiple tabs may trigger refresh around the same time
|
|
169
|
+
- Your server grace window should be at least `refreshEarlyMs + network_latency`
|
|
170
|
+
|
|
171
|
+
Recommendation: Set server grace window to 30-60 seconds to safely handle multi-tab scenarios.
|
|
172
|
+
|
|
89
173
|
## Installation
|
|
90
174
|
|
|
91
175
|
```bash
|
|
@@ -127,12 +211,18 @@ const api = createClient({
|
|
|
127
211
|
})
|
|
128
212
|
|
|
129
213
|
type User = { id: string; name: string }
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
if (
|
|
133
|
-
|
|
214
|
+
const result = await api.get('https://api.example.com/users')
|
|
215
|
+
|
|
216
|
+
if (result.ok) {
|
|
217
|
+
const envelope = result.data
|
|
218
|
+
if (envelope.status >= 200 && envelope.status < 400) {
|
|
219
|
+
const users: User[] = JSON.parse(envelope.body)
|
|
220
|
+
console.log('Users:', users)
|
|
221
|
+
} else {
|
|
222
|
+
console.error('HTTP error:', envelope.status, envelope.body)
|
|
223
|
+
}
|
|
134
224
|
} else {
|
|
135
|
-
console.error('
|
|
225
|
+
console.error('Network error:', result.errors[0].message)
|
|
136
226
|
}
|
|
137
227
|
|
|
138
228
|
// Cleanup
|
|
@@ -217,8 +307,8 @@ await api.get('/public/config', { requiresAuth: false })
|
|
|
217
307
|
|
|
218
308
|
// Include response headers in the result
|
|
219
309
|
const r = await api.get('/profile', { includeHeaders: true })
|
|
220
|
-
if (r.
|
|
221
|
-
console.log(r.status, r.headers, r.data)
|
|
310
|
+
if (r.ok) {
|
|
311
|
+
console.log(r.data.status, r.data.headers, r.data.body)
|
|
222
312
|
}
|
|
223
313
|
```
|
|
224
314
|
|
|
@@ -249,8 +339,8 @@ const result = await api.fetch('https://api.example.com/upload', {
|
|
|
249
339
|
await api.put('https://api.example.com/upload/123', formData)
|
|
250
340
|
await api.patch('https://api.example.com/upload/123', formData)
|
|
251
341
|
|
|
252
|
-
if (result.
|
|
253
|
-
console.log('Upload successful:', result.data)
|
|
342
|
+
if (result.ok && result.data.status < 400) {
|
|
343
|
+
console.log('Upload successful:', result.data.body)
|
|
254
344
|
}
|
|
255
345
|
```
|
|
256
346
|
|
|
@@ -531,8 +621,8 @@ The library targets modern browsers with Web Worker and (optionally) IndexedDB s
|
|
|
531
621
|
- `onReady(callback)`: `() => void` - Subscribe to ready event (returns unsubscribe function)
|
|
532
622
|
|
|
533
623
|
**HTTP Methods:**
|
|
534
|
-
- `fetch(url, options?)`: `Promise<Result<
|
|
535
|
-
- `get/post/put/patch/delete(...)`: `Promise<Result<
|
|
624
|
+
- `fetch(url, options?)`: `Promise<Result<FetchEnvelope>>`
|
|
625
|
+
- `get/post/put/patch/delete(...)`: `Promise<Result<FetchEnvelope>>`
|
|
536
626
|
- `fetchWithId(url, options?)`: `{ id, result, cancel }`
|
|
537
627
|
- `cancel(id)`: Cancel pending request
|
|
538
628
|
|
|
@@ -551,15 +641,72 @@ The library targets modern browsers with Web Worker and (optionally) IndexedDB s
|
|
|
551
641
|
|
|
552
642
|
Types:
|
|
553
643
|
|
|
554
|
-
-
|
|
644
|
+
- FetchEnvelope = { status: number; body: string; contentType: string; headers: Record<string, string> }
|
|
645
|
+
- `status`: HTTP status code (2xx, 3xx, 4xx, 5xx) - Worker returns ALL HTTP responses
|
|
555
646
|
- `body`: Raw string (text/JSON) or base64 (for binary content like images, PDFs)
|
|
556
647
|
- `contentType`: Content type header (always present, e.g., 'application/json', 'image/png')
|
|
557
648
|
- Use `isBinaryContentType(contentType)` to detect binary responses
|
|
558
649
|
- Use `base64ToArrayBuffer(body)` to decode binary data
|
|
650
|
+
- **Note:** Worker no longer judges HTTP status. Consumer code should check `envelope.status` to determine success/error.
|
|
559
651
|
- AuthResult = { authenticated: boolean; user?: unknown; expiresAt?: number | null }
|
|
560
652
|
- FetchGuardRequestInit extends RequestInit with:
|
|
561
653
|
- requiresAuth?: boolean // default true
|
|
562
654
|
- includeHeaders?: boolean // default false
|
|
655
|
+
- signal?: AbortSignal // for cancellation
|
|
656
|
+
|
|
657
|
+
## Helper Functions
|
|
658
|
+
|
|
659
|
+
FetchGuard exports helper functions for common Result patterns:
|
|
660
|
+
|
|
661
|
+
```ts
|
|
662
|
+
import {
|
|
663
|
+
isSuccess,
|
|
664
|
+
isClientError,
|
|
665
|
+
isServerError,
|
|
666
|
+
isNetworkError,
|
|
667
|
+
parseJson,
|
|
668
|
+
getErrorMessage,
|
|
669
|
+
matchResult,
|
|
670
|
+
ERROR_CODES
|
|
671
|
+
} from 'fetchguard'
|
|
672
|
+
|
|
673
|
+
const result = await api.get('/users')
|
|
674
|
+
|
|
675
|
+
// Simple checks
|
|
676
|
+
if (isSuccess(result)) {
|
|
677
|
+
const users = parseJson<User[]>(result)
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// Pattern matching
|
|
681
|
+
const message = matchResult(result, {
|
|
682
|
+
success: (envelope) => `Got ${parseJson(result)?.length} users`,
|
|
683
|
+
clientError: (envelope) => `Client error: ${envelope.status}`,
|
|
684
|
+
serverError: (envelope) => `Server error: ${envelope.status}`,
|
|
685
|
+
networkError: (errors) => `Network failed: ${errors[0]?.message}`
|
|
686
|
+
})
|
|
687
|
+
|
|
688
|
+
// Type-safe error code matching
|
|
689
|
+
if (!result.ok && result.errors[0]?.code === ERROR_CODES.NETWORK_ERROR) {
|
|
690
|
+
console.log('Connection failed')
|
|
691
|
+
}
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
**Available helpers:**
|
|
695
|
+
- `isSuccess(result)` - Check if 2xx response
|
|
696
|
+
- `isClientError(result)` - Check if 4xx response
|
|
697
|
+
- `isServerError(result)` - Check if 5xx response
|
|
698
|
+
- `isNetworkError(result)` - Check if network error (no response)
|
|
699
|
+
- `parseJson<T>(result)` - Safe JSON parsing with type inference
|
|
700
|
+
- `getErrorMessage(result)` - Extract error message
|
|
701
|
+
- `getErrorBody<T>(result)` - Get typed error body from HTTP errors
|
|
702
|
+
- `getStatus(result)` / `hasStatus(result, code)` - Status code helpers
|
|
703
|
+
- `matchResult(result, handlers)` - Pattern matching
|
|
704
|
+
|
|
705
|
+
**Error codes** (`ERROR_CODES`):
|
|
706
|
+
- `NETWORK_ERROR`, `REQUEST_CANCELLED`, `REQUEST_TIMEOUT`
|
|
707
|
+
- `HTTP_ERROR`, `RESPONSE_PARSE_FAILED`, `QUEUE_FULL`
|
|
708
|
+
- `LOGIN_FAILED`, `LOGOUT_FAILED`, `TOKEN_REFRESH_FAILED`, `NOT_AUTHENTICATED`
|
|
709
|
+
- `DOMAIN_NOT_ALLOWED`, `INIT_ERROR`, `UNEXPECTED`
|
|
563
710
|
|
|
564
711
|
## Message Protocol (pairs, summary)
|
|
565
712
|
|
|
@@ -571,109 +718,119 @@ Types:
|
|
|
571
718
|
|
|
572
719
|
## Error Handling
|
|
573
720
|
|
|
574
|
-
All methods return a `Result<T>` from `ts-micro-result
|
|
721
|
+
All methods return a `Result<T>` from `ts-micro-result` v3.
|
|
575
722
|
|
|
576
723
|
```ts
|
|
577
|
-
const
|
|
578
|
-
if (
|
|
579
|
-
|
|
724
|
+
const result = await api.get('/users')
|
|
725
|
+
if (result.ok) {
|
|
726
|
+
const envelope = result.data
|
|
727
|
+
// Check HTTP status - worker doesn't judge
|
|
728
|
+
if (envelope.status >= 200 && envelope.status < 400) {
|
|
729
|
+
console.log('Success:', JSON.parse(envelope.body))
|
|
730
|
+
} else {
|
|
731
|
+
console.log('HTTP error:', envelope.status, envelope.body)
|
|
732
|
+
}
|
|
580
733
|
} else {
|
|
581
|
-
|
|
582
|
-
|
|
734
|
+
// Network error only (connection failed, timeout, cancelled)
|
|
735
|
+
const err = result.errors[0]
|
|
736
|
+
console.warn(err.code, err.message)
|
|
583
737
|
}
|
|
584
738
|
```
|
|
585
739
|
|
|
586
740
|
Grouped error helpers are exported: `GeneralErrors`, `InitErrors`, `AuthErrors`, `DomainErrors`, `RequestErrors`.
|
|
587
741
|
|
|
588
|
-
### Request
|
|
742
|
+
### Request Handling (v2.0 - New Pattern)
|
|
589
743
|
|
|
590
|
-
|
|
744
|
+
In v2.0, the Worker returns ALL HTTP responses as `ok(FetchEnvelope)`. Only network failures return `err()`:
|
|
591
745
|
|
|
592
746
|
```ts
|
|
593
|
-
const
|
|
747
|
+
const result = await api.post('/data', payload)
|
|
594
748
|
|
|
595
|
-
if (
|
|
596
|
-
|
|
597
|
-
console.log('Success:', res.data.body)
|
|
598
|
-
} else {
|
|
599
|
-
const err = res.errors?.[0]
|
|
749
|
+
if (result.ok) {
|
|
750
|
+
const envelope = result.data
|
|
600
751
|
|
|
601
|
-
//
|
|
602
|
-
if (
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
752
|
+
// Success (2xx/3xx)
|
|
753
|
+
if (envelope.status >= 200 && envelope.status < 400) {
|
|
754
|
+
console.log('Success:', JSON.parse(envelope.body))
|
|
755
|
+
}
|
|
756
|
+
// HTTP errors (4xx/5xx) - still got a response!
|
|
757
|
+
else {
|
|
758
|
+
console.log(`HTTP ${envelope.status} error`)
|
|
759
|
+
console.log('Response body:', envelope.body)
|
|
606
760
|
|
|
607
|
-
// Check specific status codes
|
|
608
|
-
if (status === 404) {
|
|
761
|
+
// Check specific status codes
|
|
762
|
+
if (envelope.status === 404) {
|
|
609
763
|
console.log('Resource not found')
|
|
610
|
-
} else if (status === 401) {
|
|
764
|
+
} else if (envelope.status === 401) {
|
|
611
765
|
console.log('Unauthorized - need to login')
|
|
766
|
+
} else if (envelope.status === 422) {
|
|
767
|
+
// Parse validation errors from server
|
|
768
|
+
const errors = JSON.parse(envelope.body)
|
|
769
|
+
console.log('Validation errors:', errors)
|
|
612
770
|
}
|
|
613
771
|
}
|
|
772
|
+
} else {
|
|
773
|
+
// Network errors ONLY - no HTTP response received
|
|
774
|
+
const err = result.errors[0]
|
|
614
775
|
|
|
615
|
-
|
|
616
|
-
else if (err?.code === 'NETWORK_ERROR') {
|
|
776
|
+
if (err.code === 'NETWORK_ERROR') {
|
|
617
777
|
console.log('Connection failed - check internet')
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
// Request cancelled by user
|
|
621
|
-
else if (err?.code === 'REQUEST_CANCELLED') {
|
|
778
|
+
} else if (err.code === 'REQUEST_CANCELLED') {
|
|
622
779
|
console.log('Request was cancelled')
|
|
623
780
|
}
|
|
624
781
|
}
|
|
625
782
|
```
|
|
626
783
|
|
|
627
|
-
**Available
|
|
628
|
-
- `HTTP_ERROR` - Server returned 4xx/5xx status (includes response body in `result.meta`)
|
|
784
|
+
**Available Error Codes (network errors only):**
|
|
629
785
|
- `NETWORK_ERROR` - Connection failed, timeout, or DNS error (no response)
|
|
630
786
|
- `REQUEST_CANCELLED` - Request was cancelled via `cancel()` method
|
|
631
787
|
- `RESPONSE_PARSE_FAILED` - Failed to read/parse response body
|
|
632
788
|
|
|
633
|
-
**Key Points:**
|
|
634
|
-
- ✅
|
|
635
|
-
- ✅
|
|
636
|
-
- ✅
|
|
637
|
-
- ✅
|
|
638
|
-
- ✅
|
|
789
|
+
**Key Points (v2.0):**
|
|
790
|
+
- ✅ Worker returns `ok(envelope)` for ALL HTTP responses (2xx-5xx)
|
|
791
|
+
- ✅ Worker returns `err()` ONLY for network failures
|
|
792
|
+
- ✅ Consumer code decides what is "success" based on `envelope.status`
|
|
793
|
+
- ✅ Full response body available for HTTP errors (validation errors, etc.)
|
|
794
|
+
- ✅ Cleaner separation: transport errors vs business errors
|
|
639
795
|
|
|
640
796
|
### Auth Errors (Login/Refresh/Logout)
|
|
641
797
|
|
|
642
|
-
Auth errors
|
|
798
|
+
Auth errors include HTTP status and response body in `meta.params`:
|
|
643
799
|
|
|
644
800
|
```ts
|
|
645
801
|
const result = await api.login({ email: 'wrong@example.com', password: 'wrong' })
|
|
646
802
|
|
|
647
|
-
if (result.
|
|
803
|
+
if (!result.ok) {
|
|
648
804
|
const error = result.errors[0]
|
|
805
|
+
const params = result.meta?.params as { body?: string; status?: number }
|
|
649
806
|
|
|
650
|
-
console.log('
|
|
807
|
+
console.log('Code:', error.code) // 'LOGIN_FAILED'
|
|
651
808
|
console.log('Message:', error.message) // "Login failed"
|
|
652
|
-
console.log('
|
|
809
|
+
console.log('Status:', params?.status) // 401
|
|
810
|
+
console.log('Body:', params?.body) // '{"error": "Invalid credentials"}'
|
|
653
811
|
|
|
654
812
|
// Parse JSON error details from server
|
|
655
813
|
try {
|
|
656
|
-
const details = JSON.parse(
|
|
814
|
+
const details = JSON.parse(params?.body || '{}')
|
|
657
815
|
console.log('Server error:', details.error) // "Invalid credentials"
|
|
658
|
-
console.log('Details:', details.message) // Additional error message
|
|
659
816
|
} catch {
|
|
660
817
|
// Not JSON - use raw body
|
|
661
|
-
console.log('Raw error:',
|
|
818
|
+
console.log('Raw error:', params?.body)
|
|
662
819
|
}
|
|
663
820
|
}
|
|
664
821
|
```
|
|
665
822
|
|
|
666
823
|
**Available Auth Error Codes:**
|
|
667
|
-
- `LOGIN_FAILED` - Login failed with HTTP status and response body in `
|
|
824
|
+
- `LOGIN_FAILED` - Login failed with HTTP status and response body in `meta.params`
|
|
668
825
|
- `TOKEN_REFRESH_FAILED` - Token refresh failed with HTTP status and response body
|
|
669
826
|
- `LOGOUT_FAILED` - Logout failed with HTTP status and response body
|
|
670
827
|
- `NOT_AUTHENTICATED` - User is not authenticated (attempted auth-required request without token)
|
|
671
828
|
|
|
672
|
-
**Key Points:**
|
|
673
|
-
- ✅ Auth errors
|
|
674
|
-
- ✅ Response body available in `
|
|
829
|
+
**Key Points (v2.0):**
|
|
830
|
+
- ✅ Auth errors use `meta.params` for custom data (ts-micro-result v3 pattern)
|
|
831
|
+
- ✅ Response body available in `result.meta?.params?.body`
|
|
832
|
+
- ✅ HTTP status available in `result.meta?.params?.status`
|
|
675
833
|
- ✅ Parse JSON yourself if server returns structured error messages
|
|
676
|
-
- ✅ Consistent pattern with fetch error handling
|
|
677
834
|
|
|
678
835
|
## Auth Methods and Events
|
|
679
836
|
|
|
@@ -700,7 +857,7 @@ await api.login(credentials) // Same as above
|
|
|
700
857
|
|
|
701
858
|
// Silent (no event - useful for checks)
|
|
702
859
|
const result = await api.refreshToken(false)
|
|
703
|
-
if (result.
|
|
860
|
+
if (result.ok) {
|
|
704
861
|
const { authenticated, expiresAt } = result.data
|
|
705
862
|
// Check state without triggering listeners
|
|
706
863
|
}
|
|
@@ -735,9 +892,22 @@ FormData serialization is tested in [tests/browser/formdata-serialization.test.t
|
|
|
735
892
|
|
|
736
893
|
- SSE streaming support
|
|
737
894
|
- Upload progress tracking
|
|
738
|
-
-
|
|
739
|
-
|
|
740
|
-
|
|
895
|
+
- Offline queueing (with careful auth handling)
|
|
896
|
+
|
|
897
|
+
## Claude Code Skill
|
|
898
|
+
|
|
899
|
+
FetchGuard includes a [Claude Code](https://claude.ai/code) skill for AI-assisted development. To use it:
|
|
900
|
+
|
|
901
|
+
1. Copy the skill folder to your global Claude settings:
|
|
902
|
+
```bash
|
|
903
|
+
# macOS/Linux
|
|
904
|
+
cp -r node_modules/fetchguard/skills/fetchguard ~/.claude/skills/
|
|
905
|
+
|
|
906
|
+
# Windows
|
|
907
|
+
xcopy /E /I node_modules\fetchguard\skills\fetchguard %USERPROFILE%\.claude\skills\fetchguard
|
|
908
|
+
```
|
|
909
|
+
|
|
910
|
+
2. Use `/fetchguard` in Claude Code when working with FetchGuard in any project.
|
|
741
911
|
|
|
742
912
|
## License
|
|
743
913
|
|