Skip to content

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 ​

  1. CoSecRequestInterceptor skips a request that isTrusted rejects; otherwise it sets application/device/request headers and a truthy resolved space ID.
  2. 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.
  3. ResourceAttributionRequestInterceptor fills tenant/owner URL path parameters before URL resolution. The core transport sends the request.
  4. 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.
  5. 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.

mermaid
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 error

Interceptor parameters and results ​

ExportConstructor optionsintercept(exchange)
CoSecRequestInterceptor / CoSecRequestOptionsappId, deviceIdStorage required; spaceIdProvider defaults NoneSpaceIdProvider; optional isTrustedPromise<void>; nothing for an untrusted request; otherwise overwrites app/device/request headers; writes space only for a truthy ID; storage/provider failures propagate
AuthorizationRequestInterceptor / AuthorizationInterceptorOptionstokenManager required (JwtTokenManagerCapable); optional isTrustedPromise<void>; nothing for an untrusted request; preserves explicit Authorization, refreshes managed token when needed
AuthorizationResponseInterceptorSame AuthorizationInterceptorOptionsPromise<void>; only 401, only matching managed credential, at most AUTHORIZATION_RESPONSE_MAX_RETRY=1
ResourceAttributionRequestInterceptor / ResourceAttributionOptionstokenStorage required; tenantId='tenantId', ownerId='ownerId' are placeholder namesvoid; takes tenantId/sub from decoded access payload; fills matching template fields only when current path value is falsy
UnauthorizedErrorInterceptor / optionsonUnauthorized 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 / optionsonForbidden 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 familyValue
COSEC_REQUEST_INTERCEPTOR_NAME/ORDERCoSecRequestInterceptor / Number.MIN_SAFE_INTEGER + DEFAULT_INTERCEPTOR_ORDER_STEP
AUTHORIZATION_REQUEST_INTERCEPTOR_NAME/ORDERAuthorizationRequestInterceptor / COSEC_REQUEST_INTERCEPTOR_ORDER + DEFAULT_INTERCEPTOR_ORDER_STEP
AUTHORIZATION_RESPONSE_INTERCEPTOR_NAME/ORDERAuthorizationResponseInterceptor / Number.MIN_SAFE_INTEGER + 1000
RESOURCE_ATTRIBUTION_REQUEST_INTERCEPTOR_NAME/ORDERResourceAttributionRequestInterceptor / URL_RESOLVE_INTERCEPTOR_ORDER - DEFAULT_INTERCEPTOR_ORDER_STEP
UNAUTHORIZED_ERROR_INTERCEPTOR_NAME/ORDERUnauthorizedErrorInterceptor / 0
FORBIDDEN_ERROR_INTERCEPTOR_NAME/ORDERForbiddenErrorInterceptor / 0
DEFAULT_COSEC_DEVICE_ID_KEY, DEFAULT_COSEC_SPACE_ID_KEYcosec-device-id, cosec-space-id
CoSecHeaders static fieldsDEVICE_ID=CoSec-Device-Id, APP_ID=CoSec-App-Id, SPACE_ID=CoSec-Space-Id, AUTHORIZATION=Authorization, REQUEST_ID=CoSec-Request-Id
ResponseCodesUNAUTHORIZED=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 ​

ts
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

Released under the Apache License 2.0.