Skip to content
Back to skills

Fetcher Cosec Auth

ASecurity

Add CoSec authentication to a Fetcher client with `@ahoo-wang/fetcher-cosec`: `CoSecConfigurer`, JWT `TokenStorage`, token refresh and 401 retry, 403 handling, device/space IDs, `{tenantId}`/`{ownerId}` attribution and cross-tab sign-in state. Use for login tokens, bearer headers, refresh loops, sign-out on refresh failure or auth redirects. For generic interceptors use fetcher-integration; for React `SecurityProvider`/`RouteGuard`, fetcher-react-hooks.

  • 3 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 2, 2026
ai-agentsrustgoreactapisecurity

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 3 files and shows the line behind each finding

Scanned October 7, 2026

npx -y skills add Ahoo-Wang/skills --skill fetcher-cosec-auth --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Fetcher Cosec Auth?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Fetcher Cosec Auth
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/ahoo-wang-fetcher-cosec-auth/badge)](https://www.skillsdirectory.com/skills/ahoo-wang-fetcher-cosec-auth)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: fetcher-cosec-auth
description: >
  Add CoSec authentication to a Fetcher client with `@ahoo-wang/fetcher-cosec`: `CoSecConfigurer`, JWT `TokenStorage`, token refresh and 401 retry, 403 handling, device/space IDs, `{tenantId}`/`{ownerId}` attribution and cross-tab sign-in state. Use for login tokens, bearer headers, refresh loops, sign-out on refresh failure or auth redirects. For generic interceptors use fetcher-integration; for React `SecurityProvider`/`RouteGuard`, fetcher-react-hooks.
---

# fetcher-cosec-auth

## Decisions

- **`CoSecConfigurer` first**: `new CoSecConfigurer(config).applyTo(fetcher)` registers every interceptor in the right order; register the interceptors by hand only to customize one of them.
- **No `tokenRefresher`, no auth**: the Authorization request/response interceptors are registered only when `config.tokenRefresher` is set. Without it only the `CoSec-*` headers and resource attribution apply — no Bearer token is sent even if one is stored.
- **Trust only your origins**: by default every request, including an absolute URL on another origin (a pagination link, a download URL), receives the access token and the `CoSec-*` headers with the device ID. Pass `isTrusted: sameOriginTrust` to keep them to the `baseURL` origin and the page origin, or your own `RequestTrust`; relative URLs are always trusted, and an untrusted request is neither authorized nor refreshed.
- **401 vs 403**: `AuthorizationResponseInterceptor` refreshes and retries a 401 once (`AUTHORIZATION_RESPONSE_MAX_RETRY`); `onUnauthorized` fires when that fails, or when the refresh endpoint rejects the refresh token (not when it is merely unreachable). `onForbidden` fires on 403 and never refreshes. Neither callback clears the error — the call still rejects with `ExchangeError`, so redirects/UI go in the callbacks and callers still handle the rejection. A callback that throws makes the call reject with an `ExchangeError` whose `cause` is the thrown value.
- **React**: this skill wires the Fetcher. `SecurityProvider`, `useSecurity`, `RouteGuard` and `RefreshableRouteGuard` live in `@ahoo-wang/fetcher-react` ($fetcher-react-hooks); hand them the **same** `TokenStorage` instance (and `configurer.tokenManager`) you pass to `CoSecConfigurer`.

## Gotchas a capable model gets wrong

- Tokens enter through `tokenStorage.signIn({ accessToken, refreshToken })` after login; `signIn()` starts a new session and in-flight requests from the old one reject with `RefreshSessionChangedError`.
- A caller-set `Authorization` header (any value, case-insensitive) skips token injection and 401 refresh for that request. `IGNORE_REFRESH_TOKEN_ATTRIBUTE_KEY` in `attributes` is checked by presence, so even `false` disables refresh.
- A custom `TokenRefresher` that calls through a CoSec-configured fetcher must pass `attributes: new Map([[IGNORE_REFRESH_TOKEN_ATTRIBUTE_KEY, true]])` to avoid a refresh loop; `CoSecTokenRefresher` already does.
- Ordering: CoSec headers and Authorization run near `Number.MIN_SAFE_INTEGER` (`AUTHORIZATION_REQUEST_INTERCEPTOR_ORDER`), so your own interceptor that reads the Bearer header needs a larger `order`.
- Attribution fills `{tenantId}` / `{ownerId}` URL placeholders from the JWT (`tenantId`, `sub`) only when the caller did not supply them — write placeholders, don't interpolate the values.
- When a refresh fails because another tab already spent the one-time refresh token, the manager re-reads storage (`KeyStorage.reload()`) and continues with that tab's token for the same session instead of signing out.
- A failed refresh signs the user out (token removed, `RefreshTokenError`, `onUnauthorized`) only when the refresh endpoint rejects the refresh token: a 4xx at `error.exchange.response.status`, or a response that is not a composite token. A network error, timeout, abort or 5xx keeps the session and rejects with `RefreshUnavailableError` without calling `onUnauthorized`; a later request refreshes again. The caller sees an `ExchangeError` whose `cause` is that error — test `error.cause instanceof RefreshUnavailableError` to show "try again", not a login page. A custom `TokenRefresher` must reject with an error carrying `exchange.response.status` for a rejection to sign out; a bare `Error` keeps the session.
- In a browser, tabs sharing a `TokenStorage` on `localStorage` refresh one at a time under a Web Lock (`cosec-refresh:<key>`), so a hanging refresh blocks the other tabs: give the refresh client's Fetcher a `timeout`.
- A JWT whose payload is not a JSON object makes `parseJwtPayload` return `null`, so the token reads as expired.
- Default storage keys are `cosec-token`, `cosec-device-id` and `cosec-space-id`, synced across tabs; `destroy()` closes the broadcast bus a storage created (not one passed in `eventBus`); `TokenStorage`s sharing one `eventBus` must use the same `earlyPeriod` or the constructor throws.

## Minimal example

```ts
import { Fetcher } from '@ahoo-wang/fetcher';
import {
  CoSecConfigurer,
  CoSecTokenRefresher,
  TokenStorage,
  sameOriginTrust,
} from '@ahoo-wang/fetcher-cosec';

const fetcher = new Fetcher({
  baseURL: 'https://api.example.com',
  timeout: 10_000, // also bounds the refresh POST
});
const tokenStorage = new TokenStorage({ earlyPeriod: 60 }); // seconds
new CoSecConfigurer({
  appId: 'my-app',
  isTrusted: sameOriginTrust,
  tokenStorage,
  tokenRefresher: new CoSecTokenRefresher({
    fetcher,
    endpoint: '/auth/refresh',
  }),
  onUnauthorized: () => window.location.assign('/login'),
  onForbidden: async () => console.warn('forbidden'),
}).applyTo(fetcher);
// after login:
tokenStorage.signIn({ accessToken, refreshToken });
```

## References

- `references/api.md`: `CoSecConfig` options (including `isTrusted`), JWT token classes, `TokenStorage`/`DeviceIdStorage`, `JwtTokenManager`, `TokenRefresher`, headers, interceptor order table, space providers and complete examples. Load it for anything beyond the setup above.

## Related Skills

- $fetcher-integration: interceptor mechanics and error classes.
- $fetcher-storage: `KeyStorage`, which the token and device storages build on.
- $fetcher-react-hooks: `SecurityProvider`, `useSecurity` and `RouteGuard` in React.

Files in this skill

  • SKILL.md1.6 KB
  • agents/openai.yaml201 B
  • references/api.md17.6 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.