Security
What @mrzr/api-client protects against and what it doesn't: token isolation, trusted origins, storage trade-offs, recommended setups and a CSP.
What it protects against
| Threat | Protection |
|---|---|
| Token theft through XSS | Worker isolation plus "memory" storage: the token has no variable or storage key page code can reach |
| Token sent to the wrong server | The token and CSRF header only go to the baseUrl origin, the page's origin and authOrigins. Each URL is parsed once, and that exact URL is both checked and fetched, so tricks like //evil.com or \\evil.com get no credentials |
| Token read by page code | getAccessToken() refuses unless exposeTokens: true. Responses leaving the worker have the session's tokens removed |
| Path injection | addTemplateToUrl and addToUrl values are encoded as one path segment |
| CSRF | The double-submit header in cookie mode, if your server checks it |
| Tokens in logs | Never in LogEntry, AuthState or messages between tabs |
| Refresh token reuse | One refresh at a time, per tab and across tabs |
| A malicious dependency | There are none: zero runtime dependencies |
It doesn't address man-in-the-middle attacks: use HTTPS.
What it can't do: XSS can still use the session
Worker isolation stops the token from being stolen. It can't stop injected script from using your client: api.post("/transfer", …) from an attacker gets the token attached like any other call.
That still changes a lot:
| Without isolation | With isolation |
|---|---|
| The token is sent to the attacker's server | The token stays in the worker |
| The attack continues after the tab closes | The attack ends with the page |
| A stolen refresh token gives indefinite access | The refresh token never leaves |
It's defence in depth. Preventing XSS (a strict CSP, output encoding, careful dependencies) still comes first.
Endpoints of yours that hand out a new credential (an OAuth token exchange, an API key endpoint, a socket ticket) return it to whoever calls them, including injected script. Keep those credentials short-lived and narrowly scoped.
Storage, from safest to least safe
authMode: "cookie"with httpOnly cookies: the client never sees a token.- Header mode,
storage: "memory", worker on: the token can't be reached from the page. - Header mode,
"memory", worker off: the token sits in a closure on the page. "session": readable by any script on the page."local": readable, and persists."cookie"(not httpOnly): readable, and sent with every request to your site.
The trade-off is reloads: "memory" logs the user out on reload, cookie mode doesn't.
Recommended setups
You control the backend and share a site with it. The strongest option:
createClient({ baseUrl: "https://api.example.com", authMode: "cookie", xsrfCookieName: "csrftoken" });Set-Cookie: session=…; HttpOnly; Secure; SameSite=Strict; Path=/
Set-Cookie: csrftoken=…; Secure; SameSite=Strict; Path=/A SPA calling someone else's API. Header mode with the defaults:
createClient({ baseUrl: "https://api.vendor.com", onAuthFailure: () => router.push("/login") });Sessions must survive reloads and cookies aren't an option. Accept the XSS exposure and add a strict CSP. The worker is still worth keeping, since the live token stays inside it:
createClient({ baseUrl, storage: "local" });Content Security Policy
Content-Security-Policy:
default-src 'self';
script-src 'self';
worker-src 'self' blob:;
connect-src 'self' https://api.example.com;
object-src 'none';
base-uri 'self';
frame-ancestors 'none';worker-src blob:is required for the worker. Without it the client falls back to the main thread without an error.connect-srclimits wherefetchcan go. It's what actually stops data being sent to an attacker's domain.- Avoid
'unsafe-inline'and'unsafe-eval'inscript-src: they're what make XSS easy.
Checklist
api.isWorkeristruein production browsers, orworker: falseis deliberatestorageis"memory", or you've accepted the exposure- Cookie mode has CSRF configured, and the server enforces it
- The CSP has
worker-src blob:and a tightconnect-src baseUrlis HTTPS in productiononAuthFailureclears cached user data, not just the route- Access tokens are short-lived (15 minutes or less), and refresh tokens rotate
- No token appears in analytics or error reports
Report vulnerabilities through a security advisory, not a public issue.
Recipes
Copy-paste patterns for @mrzr/api-client: retry with backoff, pagination, polling, deduplication, case conversion, Zod validation and testing.
Troubleshooting
Fix common @mrzr/api-client problems: error messages explained, requests hitting the wrong host, logouts on reload, workers not starting and CSRF issues.