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/LICENSE +21 -0
- package/README.md +429 -0
- package/dist/index.d.ts +551 -0
- package/dist/index.js +706 -0
- package/dist/index.js.map +1 -0
- package/dist/worker.d.ts +2 -0
- package/dist/worker.js +587 -0
- package/dist/worker.js.map +1 -0
- package/package.json +69 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 FetchGuard
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
# FetchGuard
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/fetchguard)
|
|
4
|
+

|
|
5
|
+
[](https://www.npmjs.com/package/fetchguard)
|
|
6
|
+
[](https://github.com/minhtaimc/fetchguard/blob/main/LICENSE)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
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
|
+
|
|
11
|
+
## Why FetchGuard
|
|
12
|
+
|
|
13
|
+
- 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.
|
|
14
|
+
- Proactive refresh: refresh before expiry to avoid 401s and flapping UIs.
|
|
15
|
+
- Modular providers: compose Storage + Parser + Strategy or use presets.
|
|
16
|
+
- Type-safe results: powered by `ts-micro-result` (no try/catch pyramid).
|
|
17
|
+
- Public endpoints: opt out per request with `requiresAuth: false`.
|
|
18
|
+
- Domain allow-list: block requests to unexpected hosts (wildcards and ports supported).
|
|
19
|
+
|
|
20
|
+
## Architecture (Simplified)
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
+--------------------+ postMessage +-------------------------+ HTTPS +------------------+
|
|
24
|
+
| Main App (UI) | requests/results only | Web Worker (Sandbox) | fetch with auth | Backend API |
|
|
25
|
+
| - React/Vue/... | -----------------------> | - IIFE closure tokens | -----------------> | - Auth endpoints|
|
|
26
|
+
| - No token access | <----------------------- | - Domain allow-list | <----------------- | - JSON payloads |
|
|
27
|
+
+--------------------+ data only (no tokens) +-------------------------+ responses only +------------------+
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
- Setup once: app configures provider; worker is ready.
|
|
31
|
+
- Login: worker calls BE, parses tokens, stores inside closure (never exposed).
|
|
32
|
+
- Authenticated request: worker ensures token, adds Authorization, calls BE, returns data only.
|
|
33
|
+
- Public request: `requiresAuth: false` skips token injection.
|
|
34
|
+
|
|
35
|
+
Security highlight: Web Worker sandbox + IIFE closure ensures tokens never appear in `window`, storage, or any message payload.
|
|
36
|
+
|
|
37
|
+
## Closure Isolation
|
|
38
|
+
|
|
39
|
+
Inside the worker, tokens live in a lexical closure created by an IIFE. They are not properties on `self`, not exported, and never posted back.
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
// worker.ts (simplified structure)
|
|
43
|
+
;(function () {
|
|
44
|
+
let accessToken: string | null = null
|
|
45
|
+
let refreshToken: string | null = null
|
|
46
|
+
let expiresAt: number | null = null
|
|
47
|
+
let currentUser: unknown | undefined
|
|
48
|
+
|
|
49
|
+
self.onmessage = async (event) => {
|
|
50
|
+
// Handle SETUP/FETCH/AUTH_CALL/CANCEL/PING here
|
|
51
|
+
// Only status/body/headers or auth state get posted back
|
|
52
|
+
}
|
|
53
|
+
})()
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Practical effects:
|
|
57
|
+
- Scripts running in the main thread cannot read tokens (they are not in global memory or storage).
|
|
58
|
+
- `postMessage` payloads never include tokens; `AUTH_STATE_CHANGED` emits booleans/timestamps/user only.
|
|
59
|
+
- Even if an attacker obtains the `Worker` instance, there is no API surface to retrieve tokens.
|
|
60
|
+
|
|
61
|
+
## Token Storage
|
|
62
|
+
|
|
63
|
+
- Access token: worker memory only (closure; not readable by the main thread).
|
|
64
|
+
- Refresh token:
|
|
65
|
+
- Cookie provider: httpOnly cookie managed by the browser (never readable by JS).
|
|
66
|
+
- Body provider: persisted in IndexedDB via `createIndexedDBStorage` for session continuity.
|
|
67
|
+
|
|
68
|
+
## Security Model
|
|
69
|
+
|
|
70
|
+
Helps with:
|
|
71
|
+
- Prevents scripts in the main thread from reading tokens directly (no tokens in `window`, `localStorage`, or JS-readable cookies).
|
|
72
|
+
- Reduces token exfiltration risk by restricting worker fetches to an allow-listed set of domains.
|
|
73
|
+
- Avoids 401-triggered race conditions by refreshing early inside the worker.
|
|
74
|
+
|
|
75
|
+
Out of scope (still recommended to address):
|
|
76
|
+
- A fully compromised app can still ask the worker to perform actions on the user’s behalf.
|
|
77
|
+
- Malicious browser extensions or devtools can subvert runtime.
|
|
78
|
+
- Build-time or supply-chain tampering can alter provider code before it reaches the worker.
|
|
79
|
+
|
|
80
|
+
Hardening tips:
|
|
81
|
+
- Enable strict CSP and Trusted Types where applicable.
|
|
82
|
+
- Serve over HTTPS; set secure cookie attributes (httpOnly, SameSite, Secure) when using cookies.
|
|
83
|
+
- Rotate refresh tokens on every refresh/login and invalidate older tokens server-side; prefer one-time-use refresh tokens to limit replay risk.
|
|
84
|
+
- Keep tokens short-lived; rely on refresh tokens and server-side revocation.
|
|
85
|
+
- Use the domain allow-list aggressively (include explicit ports in development).
|
|
86
|
+
- Avoid logging or exposing tokens in any responses; keep login/refresh parsing inside the worker.
|
|
87
|
+
|
|
88
|
+
## Installation
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
npm install fetchguard ts-micro-result
|
|
92
|
+
# pnpm add fetchguard ts-micro-result
|
|
93
|
+
# yarn add fetchguard ts-micro-result
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Peer dependency: `ts-micro-result`.
|
|
97
|
+
|
|
98
|
+
## Quick Start
|
|
99
|
+
|
|
100
|
+
Pick a provider that matches your backend. For SPAs that return tokens in the response body, use the `body-auth` provider. For SSR/httpOnly cookie flows, use the `cookie-auth` provider.
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
import { createClient } from 'fetchguard'
|
|
104
|
+
|
|
105
|
+
const api = createClient({
|
|
106
|
+
provider: {
|
|
107
|
+
type: 'body-auth', // or 'cookie-auth'
|
|
108
|
+
refreshUrl: 'https://api.example.com/auth/refresh',
|
|
109
|
+
loginUrl: 'https://api.example.com/auth/login',
|
|
110
|
+
logoutUrl: 'https://api.example.com/auth/logout'
|
|
111
|
+
},
|
|
112
|
+
allowedDomains: ['api.example.com', '*.cdn.example.com']
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
type User = { id: string; name: string }
|
|
116
|
+
const res = await api.get<User[]>('https://api.example.com/users')
|
|
117
|
+
|
|
118
|
+
if (res.isOk()) {
|
|
119
|
+
console.log('Users:', res.data)
|
|
120
|
+
} else {
|
|
121
|
+
console.error('Error:', res.errors?.[0])
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Cleanup
|
|
125
|
+
api.destroy()
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Login / Logout
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
// Perform login; worker stores tokens and emits an auth event
|
|
132
|
+
await api.login({ email: 'user@example.com', password: 'password123' })
|
|
133
|
+
|
|
134
|
+
// Subscribe to auth state changes
|
|
135
|
+
const unsubscribe = api.onAuthStateChanged(({ authenticated, expiresAt, user }) => {
|
|
136
|
+
console.log('Auth:', authenticated, 'exp:', expiresAt, 'user:', user)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
// Later
|
|
140
|
+
await api.logout()
|
|
141
|
+
unsubscribe()
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Public Endpoints and Headers
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
// Skip auth for public endpoints
|
|
148
|
+
await api.get('/public/config', { requiresAuth: false })
|
|
149
|
+
|
|
150
|
+
// Include response headers in the result
|
|
151
|
+
const r = await api.get('/profile', { includeHeaders: true })
|
|
152
|
+
if (r.isOk()) {
|
|
153
|
+
console.log(r.status, r.headers, r.data)
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
### Cancellation
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
const { id, result, cancel } = api.fetchWithId('/slow')
|
|
162
|
+
// ... some time later
|
|
163
|
+
cancel()
|
|
164
|
+
|
|
165
|
+
const rr = await result // rejects with a cancellation error
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Provider System (Composable)
|
|
169
|
+
|
|
170
|
+
Providers are composed from three parts:
|
|
171
|
+
|
|
172
|
+
- Storage: where refresh tokens persist (e.g., IndexedDB) or none for cookie flows
|
|
173
|
+
- Parser: how to parse tokens from backend responses
|
|
174
|
+
- Strategy: how to call refresh/login/logout endpoints
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
import {
|
|
178
|
+
createProvider,
|
|
179
|
+
createIndexedDBStorage,
|
|
180
|
+
bodyParser,
|
|
181
|
+
createBodyStrategy
|
|
182
|
+
} from 'fetchguard'
|
|
183
|
+
|
|
184
|
+
const provider = createProvider({
|
|
185
|
+
// Persist refresh tokens across reloads
|
|
186
|
+
refreshStorage: createIndexedDBStorage('MyAppDB', 'refreshToken'),
|
|
187
|
+
// Parse tokens from JSON body
|
|
188
|
+
parser: bodyParser,
|
|
189
|
+
// Call auth endpoints with tokens in request body
|
|
190
|
+
strategy: createBodyStrategy({
|
|
191
|
+
refreshUrl: '/auth/refresh',
|
|
192
|
+
loginUrl: '/auth/login',
|
|
193
|
+
logoutUrl: '/auth/logout'
|
|
194
|
+
})
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Preset Providers
|
|
200
|
+
|
|
201
|
+
FetchGuard provides two built-in auth strategies:
|
|
202
|
+
|
|
203
|
+
**1. Cookie Auth** (SSR/httpOnly cookies)
|
|
204
|
+
|
|
205
|
+
Best for server-side rendered apps where tokens are managed via httpOnly cookies.
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
const api = createClient({
|
|
209
|
+
provider: {
|
|
210
|
+
type: 'cookie-auth',
|
|
211
|
+
refreshUrl: 'https://api.example.com/auth/refresh',
|
|
212
|
+
loginUrl: 'https://api.example.com/auth/login',
|
|
213
|
+
logoutUrl: 'https://api.example.com/auth/logout'
|
|
214
|
+
},
|
|
215
|
+
allowedDomains: ['api.example.com']
|
|
216
|
+
})
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
**2. Body Auth** (SPA with IndexedDB)
|
|
220
|
+
|
|
221
|
+
Best for single-page apps where tokens are returned in response body and persisted to IndexedDB.
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
const api = createClient({
|
|
225
|
+
provider: {
|
|
226
|
+
type: 'body-auth',
|
|
227
|
+
refreshUrl: 'https://api.example.com/auth/refresh',
|
|
228
|
+
loginUrl: 'https://api.example.com/auth/login',
|
|
229
|
+
logoutUrl: 'https://api.example.com/auth/logout',
|
|
230
|
+
refreshTokenKey: 'refreshToken' // Optional, defaults to 'refreshToken'
|
|
231
|
+
},
|
|
232
|
+
allowedDomains: ['api.example.com']
|
|
233
|
+
})
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Advanced: Custom Providers via Registry
|
|
237
|
+
|
|
238
|
+
For complex auth flows, you can create custom providers and register them:
|
|
239
|
+
|
|
240
|
+
```ts
|
|
241
|
+
import { registerProvider, createClient, createProvider } from 'fetchguard'
|
|
242
|
+
import { createIndexedDBStorage } from 'fetchguard'
|
|
243
|
+
import { ok } from 'ts-micro-result'
|
|
244
|
+
|
|
245
|
+
// Create custom provider
|
|
246
|
+
const myProvider = createProvider({
|
|
247
|
+
refreshStorage: createIndexedDBStorage('MyApp', 'refreshToken'),
|
|
248
|
+
parser: {
|
|
249
|
+
async parse(response: Response) {
|
|
250
|
+
const data = await response.json()
|
|
251
|
+
return {
|
|
252
|
+
token: data.accessToken,
|
|
253
|
+
refreshToken: data.refreshToken,
|
|
254
|
+
expiresAt: data.expiresAt,
|
|
255
|
+
user: data.user
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
strategy: {
|
|
260
|
+
async refreshToken(refreshToken: string | null) {
|
|
261
|
+
const res = await fetch('https://api.example.com/auth/refresh', {
|
|
262
|
+
method: 'POST',
|
|
263
|
+
headers: { 'Content-Type': 'application/json' },
|
|
264
|
+
body: JSON.stringify({ refreshToken })
|
|
265
|
+
})
|
|
266
|
+
return res
|
|
267
|
+
},
|
|
268
|
+
async login(payload: unknown) {
|
|
269
|
+
const res = await fetch('https://api.example.com/auth/login', {
|
|
270
|
+
method: 'POST',
|
|
271
|
+
headers: { 'Content-Type': 'application/json' },
|
|
272
|
+
body: JSON.stringify(payload)
|
|
273
|
+
})
|
|
274
|
+
return res
|
|
275
|
+
},
|
|
276
|
+
async logout(payload?: unknown) {
|
|
277
|
+
const res = await fetch('https://api.example.com/auth/logout', {
|
|
278
|
+
method: 'POST',
|
|
279
|
+
headers: { 'Content-Type': 'application/json' },
|
|
280
|
+
body: JSON.stringify(payload || {})
|
|
281
|
+
})
|
|
282
|
+
return res
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
// Register provider
|
|
288
|
+
registerProvider('my-custom-auth', myProvider)
|
|
289
|
+
|
|
290
|
+
// Use registered provider
|
|
291
|
+
const api = createClient({
|
|
292
|
+
provider: 'my-custom-auth', // Reference by name
|
|
293
|
+
allowedDomains: ['api.example.com']
|
|
294
|
+
})
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
### Custom Auth Methods (Advanced)
|
|
299
|
+
|
|
300
|
+
You can add custom auth methods to your provider strategy and call them via `api.call(methodName, ...args)`:
|
|
301
|
+
|
|
302
|
+
```ts
|
|
303
|
+
import { createProvider, createIndexedDBStorage, bodyParser } from 'fetchguard'
|
|
304
|
+
import { ok } from 'ts-micro-result'
|
|
305
|
+
|
|
306
|
+
const myProvider = createProvider({
|
|
307
|
+
refreshStorage: createIndexedDBStorage('MyApp', 'refreshToken'),
|
|
308
|
+
parser: bodyParser,
|
|
309
|
+
strategy: {
|
|
310
|
+
// Standard methods
|
|
311
|
+
async refreshToken(refreshToken: string | null) { /* ... */ },
|
|
312
|
+
async login(payload: unknown) { /* ... */ },
|
|
313
|
+
async logout(payload?: unknown) { /* ... */ },
|
|
314
|
+
|
|
315
|
+
// Custom method - OTP login
|
|
316
|
+
async loginWithOTP(payload: { phone: string; code: string }) {
|
|
317
|
+
const res = await fetch('https://api.example.com/auth/login/otp', {
|
|
318
|
+
method: 'POST',
|
|
319
|
+
headers: { 'Content-Type': 'application/json' },
|
|
320
|
+
body: JSON.stringify(payload)
|
|
321
|
+
})
|
|
322
|
+
return res
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
// Register and use
|
|
328
|
+
registerProvider('otp-auth', myProvider)
|
|
329
|
+
const api = createClient({ provider: 'otp-auth' })
|
|
330
|
+
|
|
331
|
+
// Call custom method
|
|
332
|
+
await api.call('loginWithOTP', { phone: '+1234567890', code: '123456' })
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
## Domain Allow-List
|
|
336
|
+
|
|
337
|
+
Limit requests to known hosts. Supports wildcards and optional ports.
|
|
338
|
+
|
|
339
|
+
```ts
|
|
340
|
+
allowedDomains: [
|
|
341
|
+
'api.example.com', // exact host
|
|
342
|
+
'*.example.com', // any subdomain
|
|
343
|
+
'localhost:5173' // include port match
|
|
344
|
+
]
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
## Bundler Notes
|
|
348
|
+
|
|
349
|
+
FetchGuard creates a Worker using `new Worker(new URL('./worker.js', import.meta.url), { type: 'module' })`.
|
|
350
|
+
|
|
351
|
+
- Vite/Rollup: supported out of the box
|
|
352
|
+
- Webpack 5: supported via `new URL()` ESM pattern
|
|
353
|
+
- Older toolchains may require a custom worker loader
|
|
354
|
+
|
|
355
|
+
The library targets modern browsers with Web Worker and (optionally) IndexedDB support.
|
|
356
|
+
|
|
357
|
+
## API Reference
|
|
358
|
+
|
|
359
|
+
### `createClient(options)`
|
|
360
|
+
|
|
361
|
+
- `provider`: `ProviderPresetConfig | string` (required)
|
|
362
|
+
- Config object: `{ type: 'cookie-auth' | 'body-auth', refreshUrl, loginUrl, logoutUrl, ... }`
|
|
363
|
+
- String: Registered provider name
|
|
364
|
+
- `allowedDomains?`: `string[]` - Domain whitelist (supports wildcards)
|
|
365
|
+
- `debug?`: `boolean` - Enable debug logging in worker
|
|
366
|
+
- `refreshEarlyMs?`: `number` - Refresh token X ms before expiry (default: 60000)
|
|
367
|
+
- `defaultTimeoutMs?`: `number` - Request timeout (default: 120000)
|
|
368
|
+
- `retryCount?`: `number` - Retry failed requests (default: 3)
|
|
369
|
+
- `retryDelayMs?`: `number` - Delay between retries (default: 1000)
|
|
370
|
+
|
|
371
|
+
- FetchGuardClient
|
|
372
|
+
- fetch(url, options?): Result<ApiResponse<T>>
|
|
373
|
+
- get/post/put/patch/delete(...): Result<ApiResponse<T>>
|
|
374
|
+
- fetchWithId(url, options?): { id, result, cancel }
|
|
375
|
+
- cancel(id)
|
|
376
|
+
- init(payload?): Result<{ initialized: true; authenticated: boolean; expiresAt?: number | null; user?: unknown }>
|
|
377
|
+
- login(payload?): Result<void>
|
|
378
|
+
- logout(payload?): Result<void>
|
|
379
|
+
- call(method: string, ...args): Result<void> // for custom provider methods
|
|
380
|
+
- onAuthStateChanged(cb): () => void
|
|
381
|
+
- ping(): Result<{ timestamp: number }>
|
|
382
|
+
- destroy(): void
|
|
383
|
+
|
|
384
|
+
Types:
|
|
385
|
+
|
|
386
|
+
- ApiResponse<T> = { data: T; status: number; headers: Record<string, string> }
|
|
387
|
+
- FetchGuardRequestInit extends RequestInit with:
|
|
388
|
+
- requiresAuth?: boolean // default true
|
|
389
|
+
- includeHeaders?: boolean // default false
|
|
390
|
+
|
|
391
|
+
## Message Protocol (pairs, summary)
|
|
392
|
+
|
|
393
|
+
- Main -> Worker: SETUP -> Worker -> Main: READY
|
|
394
|
+
- Main -> Worker: FETCH -> Worker -> Main: FETCH_RESULT | FETCH_ERROR
|
|
395
|
+
- Main -> Worker: AUTH_CALL(login/logout/...) -> Worker -> Main: RESULT (and AUTH_STATE_CHANGED event)
|
|
396
|
+
- Main -> Worker: CANCEL -> aborts in-worker fetch (no explicit response)
|
|
397
|
+
- Main -> Worker: PING -> Worker -> Main: PONG
|
|
398
|
+
|
|
399
|
+
## Error Handling
|
|
400
|
+
|
|
401
|
+
All methods return a `Result<T>` from `ts-micro-result`.
|
|
402
|
+
|
|
403
|
+
```ts
|
|
404
|
+
const res = await api.get('/users')
|
|
405
|
+
if (res.isOk()) {
|
|
406
|
+
console.log(res.data)
|
|
407
|
+
} else {
|
|
408
|
+
const err = res.errors?.[0]
|
|
409
|
+
console.warn(err?.code, err?.message)
|
|
410
|
+
}
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
Grouped error helpers are exported: `GeneralErrors`, `InitErrors`, `AuthErrors`, `DomainErrors`, `NetworkErrors`, `RequestErrors`.
|
|
414
|
+
|
|
415
|
+
## Roadmap
|
|
416
|
+
|
|
417
|
+
- SSE streaming support
|
|
418
|
+
- Upload progress
|
|
419
|
+
- Interceptors
|
|
420
|
+
- Advanced retries (exponential backoff)
|
|
421
|
+
- Offline queueing
|
|
422
|
+
|
|
423
|
+
## License
|
|
424
|
+
|
|
425
|
+
MIT - see `LICENSE`.
|
|
426
|
+
|
|
427
|
+
---
|
|
428
|
+
|
|
429
|
+
Made for secure, resilient frontend API calls.
|