Interceptors and attribution
These interceptors enrich or retry a FetchExchange. Register each with the matching request, response or error manager using .use(instance). The configurer installs the common combination.
Actual execution flow
- CoSecRequestInterceptor skips a request that
isTrustedrejects; otherwise it sets application/device/request headers and a truthy resolved space ID. - AuthorizationRequestInterceptor skips an untrusted request (no Authorization, no refresh) and preserves an existing Authorization header; otherwise it checks session ownership, refreshes when access is expired and refresh is valid, then injects the managed Bearer token.
- ResourceAttributionRequestInterceptor fills tenant/owner URL path parameters before URL resolution. The core transport sends the request.
- AuthorizationResponseInterceptor handles a managed-credential 401 before normal status validation. It refreshes, deletes only its injected stale credential, and re-executes the full exchange pipeline at most once.
- Remaining failures reach error interceptors. Unauthorized handles 401/RefreshTokenError with notification ownership guards (a
RefreshUnavailableError— the refresh could not reach the server — keeps the session and is not notified); Forbidden handles 403. Neither callback automatically recovers the failed request.
The request/response managers sort order values; registration order alone is not the execution contract. Re-executing the pipeline can create a new CoSec request ID. Caller-provided Authorization values are not replaced or automatically refreshed. A token may still be attached when access is expired and refresh is unavailable; the server decides the resulting status.
sequenceDiagram
autonumber
participant App
participant Fetcher
participant Tokens
participant Service
App->>Fetcher: Request
Fetcher->>Tokens: Read managed session
opt Expired access and valid refresh
Tokens->>Service: POST refresh
Service-->>Tokens: CompositeToken
end
Fetcher->>Service: Request with managed Authorization
Service-->>Fetcher: Response
opt Managed 401, retry not yet used
Fetcher->>Tokens: Refresh same session
Tokens-->>Fetcher: Current token or error
Fetcher->>Service: Re-run exchange once
Service-->>Fetcher: Retry response
end
Fetcher-->>App: Extracted result or errorInterceptor parameters and results
| Export | Constructor options | intercept(exchange) |
|---|---|---|
CoSecRequestInterceptor / CoSecRequestOptions | appId, deviceIdStorage required; spaceIdProvider defaults NoneSpaceIdProvider; optional isTrusted | Promise<void>; nothing for an untrusted request; otherwise overwrites app/device/request headers; writes space only for a truthy ID; storage/provider failures propagate |
AuthorizationRequestInterceptor / AuthorizationInterceptorOptions | tokenManager required (JwtTokenManagerCapable); optional isTrusted | Promise<void>; nothing for an untrusted request; preserves explicit Authorization, refreshes managed token when needed |
AuthorizationResponseInterceptor | Same AuthorizationInterceptorOptions | Promise<void>; only 401, only matching managed credential, at most AUTHORIZATION_RESPONSE_MAX_RETRY=1 |
ResourceAttributionRequestInterceptor / ResourceAttributionOptions | tokenStorage required; tenantId='tenantId', ownerId='ownerId' are placeholder names | void; takes tenantId/sub from decoded access payload; fills matching template fields only when current path value is falsy |
UnauthorizedErrorInterceptor / options | onUnauthorized required, returns void or Promise<void> | Promise<void>; skips RefreshSessionChangedError, RefreshUnavailableError and duplicate/obsolete notifications; a callback error becomes the exchange error (ExchangeError cause) |
ForbiddenErrorInterceptor / options | onForbidden required, returns Promise<void> | Promise<void>; callback runs only for response.status=403; a callback error becomes the exchange error (ExchangeError cause) |
IGNORE_REFRESH_TOKEN_ATTRIBUTE_KEY is Ignore-Refresh-Token. Presence, even with false, disables automatic proactive/401 refresh. It does not suppress Authorization injection or disable ordinary HTTP status errors.
Request trust
RequestTrust is (url: string, exchange: FetchExchange) => boolean; RequestTrustCapable adds the optional isTrusted to CoSecConfig, CoSecRequestOptions and AuthorizationInterceptorOptions. isTrustedRequest(exchange, isTrusted?) decides for both request interceptors: without isTrusted every request is trusted; with it a relative request URL is always trusted (it resolves against the baseURL) and an absolute one only when isTrusted(url, exchange) returns true. The decision is made once per exchange (per predicate) and kept on it: the 401 retry, which sees the URL already resolved against the baseURL, reuses it, so a trusted relative request keeps its credentials on the retry and isTrusted is asked once per request. sameOriginTrust trusts an absolute URL on the origin of the fetcher's baseURL or on the page's own origin; every other origin, and a URL that does not parse, is untrusted.
Default trusts every origin
By default an absolute request URL on any origin receives the access token and the CoSec headers, device ID included. Configure isTrusted: sameOriginTrust (or your own predicate) when the client requests URLs it does not control, such as pagination links, download URLs or callbacks.
Device and space selection
DeviceIdStorage(options={}) and SpaceIdStorage(options={}) extend KeyStorage<string>. Options are partial KeyStorageOptions; each forces its identity serializer. Their default keys are cosec-device-id and cosec-space-id; default buses broadcast with a serial delegate named for the actual key, and destroy() closes such a default bus; a bus passed in eventBus stays open. Storage selection is inherited from KeyStorage. Custom event buses/storage can be injected; cleanup follows KeyStorage.
DeviceIdStorage.generateDeviceId(): string calls the exported idGenerator, but does not store the result. getOrCreate(): string returns a truthy stored ID or generates/stores a new one. IdGenerator.generateId(): string is implemented by NanoIdGenerator with nanoid; idGenerator is its shared instance.
SpaceIdProvider.resolveSpaceId(exchange): string | null is synchronous. NoneSpaceIdProvider always returns null. DefaultSpaceIdProvider({ spacedResourcePredicate, spaceIdStorage }) requires both; calls SpacedResourcePredicate.test(exchange): boolean, returning stored space only for a match. It does not infer space from URLs or tokens without your predicate/storage.
Constants and authorization data
| Export family | Value |
|---|---|
COSEC_REQUEST_INTERCEPTOR_NAME/ORDER | CoSecRequestInterceptor / Number.MIN_SAFE_INTEGER + DEFAULT_INTERCEPTOR_ORDER_STEP |
AUTHORIZATION_REQUEST_INTERCEPTOR_NAME/ORDER | AuthorizationRequestInterceptor / COSEC_REQUEST_INTERCEPTOR_ORDER + DEFAULT_INTERCEPTOR_ORDER_STEP |
AUTHORIZATION_RESPONSE_INTERCEPTOR_NAME/ORDER | AuthorizationResponseInterceptor / Number.MIN_SAFE_INTEGER + 1000 |
RESOURCE_ATTRIBUTION_REQUEST_INTERCEPTOR_NAME/ORDER | ResourceAttributionRequestInterceptor / URL_RESOLVE_INTERCEPTOR_ORDER - DEFAULT_INTERCEPTOR_ORDER_STEP |
UNAUTHORIZED_ERROR_INTERCEPTOR_NAME/ORDER | UnauthorizedErrorInterceptor / 0 |
FORBIDDEN_ERROR_INTERCEPTOR_NAME/ORDER | ForbiddenErrorInterceptor / 0 |
DEFAULT_COSEC_DEVICE_ID_KEY, DEFAULT_COSEC_SPACE_ID_KEY | cosec-device-id, cosec-space-id |
CoSecHeaders static fields | DEVICE_ID=CoSec-Device-Id, APP_ID=CoSec-App-Id, SPACE_ID=CoSec-Space-Id, AUTHORIZATION=Authorization, REQUEST_ID=CoSec-Request-Id |
ResponseCodes | UNAUTHORIZED=401, FORBIDDEN=403 |
AuthorizeResult is { authorized:boolean, reason:string }. AuthorizeResults contains ALLOW (true, 'Allow'), EXPLICIT_DENY ('Explicit Deny'), IMPLICIT_DENY ('Implicit Deny'), TOKEN_EXPIRED ('Token Expired'), TOO_MANY_REQUESTS ('Too Many Requests'); all except ALLOW have authorized=false. These are result objects, not a local policy engine or server status-code mapper.
Complete space configuration
import { Fetcher } from '@ahoo-wang/fetcher';
import {
CoSecConfigurer,
DefaultSpaceIdProvider,
SpaceIdStorage,
TokenStorage,
DeviceIdStorage,
} from '@ahoo-wang/fetcher-cosec';
import { InMemoryStorage } from '@ahoo-wang/fetcher-storage';
import { SerialTypedEventBus } from '@ahoo-wang/fetcher-eventbus';
const spaces = new SpaceIdStorage({
storage: new InMemoryStorage(),
eventBus: new SerialTypedEventBus('example-space'),
});
spaces.set('workspace-1');
const provider = new DefaultSpaceIdProvider({
spaceIdStorage: spaces,
spacedResourcePredicate: {
test: exchange => exchange.request.url.startsWith('/projects'),
},
});
const fetcher = new Fetcher({ baseURL: 'https://api.example.com' });
const cosec = new CoSecConfigurer({
appId: 'example-app',
spaceIdProvider: provider,
tokenStorage: new TokenStorage({
storage: new InMemoryStorage(),
eventBus: new SerialTypedEventBus('example-token'),
}),
deviceIdStorage: new DeviceIdStorage({
storage: new InMemoryStorage(),
eventBus: new SerialTypedEventBus('example-device'),
}),
});
cosec.applyTo(fetcher);
// No request is made by constructing this configuration.
spaces.destroy();
cosec.tokenStorage.destroy();
cosec.deviceIdStorage.destroy();AuthorizationInterceptorOptions — packages/cosec/src/authorizationRequestInterceptor.ts:30
AUTHORIZATION_REQUEST_INTERCEPTOR_NAME — packages/cosec/src/authorizationRequestInterceptor.ts:33
AUTHORIZATION_REQUEST_INTERCEPTOR_ORDER — packages/cosec/src/authorizationRequestInterceptor.ts:35
AuthorizationRequestInterceptor — packages/cosec/src/authorizationRequestInterceptor.ts:48
AUTHORIZATION_RESPONSE_INTERCEPTOR_NAME — packages/cosec/src/authorizationResponseInterceptor.ts:34
AUTHORIZATION_RESPONSE_INTERCEPTOR_ORDER — packages/cosec/src/authorizationResponseInterceptor.ts:41
AUTHORIZATION_RESPONSE_MAX_RETRY — packages/cosec/src/authorizationResponseInterceptor.ts:50
AuthorizationResponseInterceptor — packages/cosec/src/authorizationResponseInterceptor.ts:69
CoSecRequestOptions — packages/cosec/src/cosecRequestInterceptor.ts:59
COSEC_REQUEST_INTERCEPTOR_NAME — packages/cosec/src/cosecRequestInterceptor.ts:85
COSEC_REQUEST_INTERCEPTOR_ORDER — packages/cosec/src/constants.ts:46
IGNORE_REFRESH_TOKEN_ATTRIBUTE_KEY — packages/cosec/src/constants.ts:73
CoSecRequestInterceptor — packages/cosec/src/cosecRequestInterceptor.ts:174
RequestTrust — packages/cosec/src/requestTrust.ts:26
RequestTrustCapable — packages/cosec/src/requestTrust.ts:28
sameOriginTrust — packages/cosec/src/requestTrust.ts:57
isTrustedRequest — packages/cosec/src/requestTrust.ts:73
DEFAULT_COSEC_DEVICE_ID_KEY — packages/cosec/src/deviceIdStorage.ts:22
DeviceIdStorageOptions — packages/cosec/src/deviceIdStorage.ts:25
DeviceIdStorage — packages/cosec/src/deviceIdStorage.ts:32
IdGenerator — packages/cosec/src/idGenerator.ts:16
NanoIdGenerator — packages/cosec/src/idGenerator.ts:24
idGenerator — packages/cosec/src/idGenerator.ts:35
ResourceAttributionOptions — packages/cosec/src/resourceAttributionRequestInterceptor.ts:27
RESOURCE_ATTRIBUTION_REQUEST_INTERCEPTOR_NAME — packages/cosec/src/resourceAttributionRequestInterceptor.ts:45
RESOURCE_ATTRIBUTION_REQUEST_INTERCEPTOR_ORDER — packages/cosec/src/resourceAttributionRequestInterceptor.ts:53
ResourceAttributionRequestInterceptor — packages/cosec/src/resourceAttributionRequestInterceptor.ts:61
SpaceIdProvider — packages/cosec/src/spaceIdProvider.ts:67
NoneSpaceIdProvider — packages/cosec/src/spaceIdProvider.ts:123
DEFAULT_COSEC_SPACE_ID_KEY — packages/cosec/src/spaceIdProvider.ts:134
SpaceIdStorageOptions — packages/cosec/src/spaceIdProvider.ts:169
SpaceIdStorage — packages/cosec/src/spaceIdProvider.ts:210
SpacedResourcePredicate — packages/cosec/src/spaceIdProvider.ts:289
SpaceIdProviderOptions — packages/cosec/src/spaceIdProvider.ts:318
DefaultSpaceIdProvider — packages/cosec/src/spaceIdProvider.ts:375
CoSecHeaders — packages/cosec/src/types.ts:20
ResponseCodes — packages/cosec/src/types.ts:28
AuthorizeResult — packages/cosec/src/types.ts:57
AuthorizeResults — packages/cosec/src/types.ts:65
UNAUTHORIZED_ERROR_INTERCEPTOR_NAME — packages/cosec/src/constants.ts:23
UNAUTHORIZED_ERROR_INTERCEPTOR_ORDER — packages/cosec/src/unauthorizedErrorInterceptor.ts:28
UnauthorizedErrorInterceptorOptions — packages/cosec/src/unauthorizedErrorInterceptor.ts:33
UnauthorizedErrorInterceptor — packages/cosec/src/unauthorizedErrorInterceptor.ts:73
FORBIDDEN_ERROR_INTERCEPTOR_NAME — packages/cosec/src/forbiddenErrorInterceptor.ts:21
FORBIDDEN_ERROR_INTERCEPTOR_ORDER — packages/cosec/src/forbiddenErrorInterceptor.ts:27
ForbiddenErrorInterceptorOptions — packages/cosec/src/forbiddenErrorInterceptor.ts:32
ForbiddenErrorInterceptor — packages/cosec/src/forbiddenErrorInterceptor.ts:113
