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 sets application/device/request headers and a truthy resolved space ID.
  2. AuthorizationRequestInterceptor preserves an existing Authorization header; otherwise 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; 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 NoneSpaceIdProviderPromise<void>; overwrites app/device/request headers; writes space only for a truthy ID; storage/provider failures propagate
AuthorizationRequestInterceptor / AuthorizationInterceptorOptionstokenManager required (JwtTokenManagerCapable)Promise<void>; 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 and duplicate/obsolete notifications; callback error propagates
ForbiddenErrorInterceptor / optionsonForbidden required, returns Promise<void>Promise<void>; callback runs only for response.status=403; callback error propagates

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.

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. 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();

AuthorizationInterceptorOptionspackages/cosec/src/authorizationRequestInterceptor.ts:29

AUTHORIZATION_REQUEST_INTERCEPTOR_NAMEpackages/cosec/src/authorizationRequestInterceptor.ts:31

AUTHORIZATION_REQUEST_INTERCEPTOR_ORDERpackages/cosec/src/authorizationRequestInterceptor.ts:33

AuthorizationRequestInterceptorpackages/cosec/src/authorizationRequestInterceptor.ts:46

AUTHORIZATION_RESPONSE_INTERCEPTOR_NAMEpackages/cosec/src/authorizationResponseInterceptor.ts:31

AUTHORIZATION_RESPONSE_INTERCEPTOR_ORDERpackages/cosec/src/authorizationResponseInterceptor.ts:38

AUTHORIZATION_RESPONSE_MAX_RETRYpackages/cosec/src/authorizationResponseInterceptor.ts:47

AuthorizationResponseInterceptorpackages/cosec/src/authorizationResponseInterceptor.ts:66

CoSecRequestOptionspackages/cosec/src/cosecRequestInterceptor.ts:57

COSEC_REQUEST_INTERCEPTOR_NAMEpackages/cosec/src/cosecRequestInterceptor.ts:83

COSEC_REQUEST_INTERCEPTOR_ORDERpackages/cosec/src/cosecRequestInterceptor.ts:104

IGNORE_REFRESH_TOKEN_ATTRIBUTE_KEYpackages/cosec/src/cosecRequestInterceptor.ts:131

CoSecRequestInterceptorpackages/cosec/src/cosecRequestInterceptor.ts:215

DEFAULT_COSEC_DEVICE_ID_KEYpackages/cosec/src/deviceIdStorage.ts:25

DeviceIdStorageOptionspackages/cosec/src/deviceIdStorage.ts:28

DeviceIdStoragepackages/cosec/src/deviceIdStorage.ts:35

IdGeneratorpackages/cosec/src/idGenerator.ts:16

NanoIdGeneratorpackages/cosec/src/idGenerator.ts:24

idGeneratorpackages/cosec/src/idGenerator.ts:35

ResourceAttributionOptionspackages/cosec/src/resourceAttributionRequestInterceptor.ts:27

RESOURCE_ATTRIBUTION_REQUEST_INTERCEPTOR_NAMEpackages/cosec/src/resourceAttributionRequestInterceptor.ts:45

RESOURCE_ATTRIBUTION_REQUEST_INTERCEPTOR_ORDERpackages/cosec/src/resourceAttributionRequestInterceptor.ts:50

ResourceAttributionRequestInterceptorpackages/cosec/src/resourceAttributionRequestInterceptor.ts:58

SpaceIdProviderpackages/cosec/src/spaceIdProvider.ts:70

NoneSpaceIdProviderpackages/cosec/src/spaceIdProvider.ts:126

DEFAULT_COSEC_SPACE_ID_KEYpackages/cosec/src/spaceIdProvider.ts:137

SpaceIdStorageOptionspackages/cosec/src/spaceIdProvider.ts:172

SpaceIdStoragepackages/cosec/src/spaceIdProvider.ts:213

SpacedResourcePredicatepackages/cosec/src/spaceIdProvider.ts:297

SpaceIdProviderOptionspackages/cosec/src/spaceIdProvider.ts:326

DefaultSpaceIdProviderpackages/cosec/src/spaceIdProvider.ts:383

CoSecHeaderspackages/cosec/src/types.ts:20

ResponseCodespackages/cosec/src/types.ts:28

AuthorizeResultpackages/cosec/src/types.ts:57

AuthorizeResultspackages/cosec/src/types.ts:65

UNAUTHORIZED_ERROR_INTERCEPTOR_NAMEpackages/cosec/src/unauthorizedErrorInterceptor.ts:24

UNAUTHORIZED_ERROR_INTERCEPTOR_ORDERpackages/cosec/src/unauthorizedErrorInterceptor.ts:31

UnauthorizedErrorInterceptorOptionspackages/cosec/src/unauthorizedErrorInterceptor.ts:36

UnauthorizedErrorInterceptorpackages/cosec/src/unauthorizedErrorInterceptor.ts:76

FORBIDDEN_ERROR_INTERCEPTOR_NAMEpackages/cosec/src/forbiddenErrorInterceptor.ts:21

FORBIDDEN_ERROR_INTERCEPTOR_ORDERpackages/cosec/src/forbiddenErrorInterceptor.ts:27

ForbiddenErrorInterceptorOptionspackages/cosec/src/forbiddenErrorInterceptor.ts:32

ForbiddenErrorInterceptorpackages/cosec/src/forbiddenErrorInterceptor.ts:113

Released under the Apache License 2.0.