@ahoo-wang/fetcher-cosec 3.13.15 → 3.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/authorizationRequestInterceptor.d.ts.map +1 -1
- package/dist/authorizationResponseInterceptor.d.ts.map +1 -1
- package/dist/cosecConfigurer.d.ts.map +1 -1
- package/dist/cosecRequestInterceptor.d.ts.map +1 -1
- package/dist/deviceIdStorage.d.ts +1 -1
- package/dist/deviceIdStorage.d.ts.map +1 -1
- package/dist/forbiddenErrorInterceptor.d.ts.map +1 -1
- package/dist/index.es.js +269 -925
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +2 -2
- package/dist/index.umd.js.map +1 -1
- package/dist/jwtToken.d.ts.map +1 -1
- package/dist/jwtTokenManager.d.ts.map +1 -1
- package/dist/resourceAttributionRequestInterceptor.d.ts.map +1 -1
- package/dist/spaceIdProvider.d.ts +1 -1
- package/dist/spaceIdProvider.d.ts.map +1 -1
- package/dist/tokenRefresher.d.ts.map +1 -1
- package/dist/tokenStorage.d.ts +1 -1
- package/dist/tokenStorage.d.ts.map +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/unauthorizedErrorInterceptor.d.ts.map +1 -1
- package/package.json +6 -6
package/dist/index.es.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.es.js","sources":["../src/types.ts","../src/idGenerator.ts","../src/spaceIdProvider.ts","../src/cosecRequestInterceptor.ts","../src/authorizationRequestInterceptor.ts","../src/authorizationResponseInterceptor.ts","../src/deviceIdStorage.ts","../src/forbiddenErrorInterceptor.ts","../src/jwtTokenManager.ts","../src/resourceAttributionRequestInterceptor.ts","../src/jwts.ts","../src/jwtToken.ts","../src/tokenStorage.ts","../src/unauthorizedErrorInterceptor.ts","../src/cosecConfigurer.ts","../src/tokenRefresher.ts"],"sourcesContent":["/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { DeviceIdStorage } from './deviceIdStorage';\nimport { JwtTokenManager } from './jwtTokenManager';\n\n/**\n * CoSec HTTP headers enumeration.\n */\nexport class CoSecHeaders {\n static readonly DEVICE_ID = 'CoSec-Device-Id';\n static readonly APP_ID = 'CoSec-App-Id';\n static readonly SPACE_ID = 'CoSec-Space-Id';\n static readonly AUTHORIZATION = 'Authorization';\n static readonly REQUEST_ID = 'CoSec-Request-Id';\n}\n\nexport class ResponseCodes {\n static readonly UNAUTHORIZED = 401;\n static readonly FORBIDDEN = 403;\n}\n\nexport interface AppIdCapable {\n /**\n * Application ID to be sent in the CoSec-App-Id header.\n */\n appId: string;\n}\n\nexport interface DeviceIdStorageCapable {\n deviceIdStorage: DeviceIdStorage;\n}\n\nexport interface JwtTokenManagerCapable {\n tokenManager: JwtTokenManager;\n}\n\n/**\n * CoSec options interface.\n */\nexport interface CoSecOptions\n extends AppIdCapable,\n DeviceIdStorageCapable,\n JwtTokenManagerCapable {\n}\n\n/**\n * Authorization result interface.\n */\nexport interface AuthorizeResult {\n authorized: boolean;\n reason: string;\n}\n\n/**\n * Authorization result constants.\n */\nexport const AuthorizeResults = {\n ALLOW: { authorized: true, reason: 'Allow' },\n EXPLICIT_DENY: { authorized: false, reason: 'Explicit Deny' },\n IMPLICIT_DENY: { authorized: false, reason: 'Implicit Deny' },\n TOKEN_EXPIRED: { authorized: false, reason: 'Token Expired' },\n TOO_MANY_REQUESTS: { authorized: false, reason: 'Too Many Requests' },\n};\n","/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { nanoid } from 'nanoid';\n\nexport interface IdGenerator {\n generateId(): string;\n}\n\n/**\n * Nano ID implementation of IdGenerator.\n * Generates unique request IDs using Nano ID.\n */\nexport class NanoIdGenerator implements IdGenerator {\n /**\n * Generate a unique request ID.\n *\n * @returns A unique request ID\n */\n generateId(): string {\n return nanoid();\n }\n}\n\nexport const idGenerator = new NanoIdGenerator();\n","/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n KeyStorage,\n KeyStorageOptions,\n typedIdentitySerializer,\n} from '@ahoo-wang/fetcher-storage';\nimport {\n BroadcastTypedEventBus,\n SerialTypedEventBus,\n} from '@ahoo-wang/fetcher-eventbus';\nimport { FetchExchange } from '@ahoo-wang/fetcher';\n\n/**\n * Represents a space identifier for resource scoping within a tenant.\n *\n * A SpaceId uniquely identifies a space (workspace, project, organization unit, etc.)\n * that belongs to a tenant. This enables multi-tenant applications to further\n * categorize resources into discrete spaces for access control and data isolation.\n *\n * In a typical multi-tenant architecture:\n * - TenantId: Identifies the tenant/organization\n * - SpaceId: Identifies a space within that tenant (e.g., team, project, department)\n *\n * @example\n * ```\n * // A tenant might have multiple spaces:\n * Tenant: \"company-abc\"\n * Space: \"engineering-team\"\n * Space: \"marketing-team\"\n * Space: \"project-alpha\"\n * ```\n */\ntype SpaceId = string;\n\n/**\n * Interface for resolving space identifiers from HTTP request exchanges.\n *\n * A SpaceIdProvider determines which space a particular request belongs to\n * based on the incoming request characteristics. This abstraction allows\n * different strategies for space identification, such as header-based,\n * URL-based, or cookie-based resolution.\n *\n * @remarks\n * Implementations of this interface are used by the CoSecRequestInterceptor\n * to add the appropriate space identifier to outgoing requests. This enables\n * proper routing and access control in multi-tenant, multi-space environments.\n *\n * @example\n * ```typescript\n * // Example: Extract space ID from a custom header\n * const headerSpaceIdProvider: SpaceIdProvider = {\n * resolveSpaceId: (exchange) => {\n * return exchange.request.headers['X-Space-Id'] || null;\n * }\n * };\n * ```\n */\nexport interface SpaceIdProvider {\n /**\n * Resolves the space identifier for a given fetch exchange.\n *\n * This method examines the request exchange and determines which space\n * the request should be associated with. The resolution strategy depends\n * on the implementation - it may examine headers, URL patterns, cookies,\n * or other request attributes.\n *\n * @param exchange - The fetch exchange containing the HTTP request details\n * @returns The resolved space identifier, or null if no space can be determined\n *\n * @example\n * ```typescript\n * // Example: Extract space ID from a custom header\n * const provider: SpaceIdProvider = {\n * resolveSpaceId: (exchange) => {\n * return exchange.request.headers['X-Space-Id'] || null;\n * }\n * };\n *\n * // Example: Extract space ID from URL path\n * const urlSpaceIdProvider: SpaceIdProvider = {\n * resolveSpaceId: (exchange) => {\n * const match = exchange.request.url.match(/\\/spaces\\/([^\\/]+)/);\n * return match ? match[1] : null;\n * }\n * };\n * ```\n */\n resolveSpaceId(exchange: FetchExchange): SpaceId | null;\n}\n\n/**\n * A no-op SpaceIdProvider that always returns null.\n *\n * This provider is used when space identification is not required or when\n * the application operates without space-level resource isolation.\n * It provides a convenient default that requires no configuration.\n *\n * @remarks\n * Use NoneSpaceIdProvider when:\n * - Resources are not organized into spaces\n * - Space context is managed externally\n * - Single-space per tenant deployment\n *\n * @example\n * ```typescript\n * // Single-space configuration (no space-level isolation needed)\n * const interceptor = new CoSecRequestInterceptor({\n * appId: 'my-app',\n * deviceIdStorage,\n * spaceIdProvider: NoneSpaceIdProvider,\n * });\n * ```\n */\nexport const NoneSpaceIdProvider: SpaceIdProvider = {\n resolveSpaceId: () => null,\n};\n\n/**\n * The default storage key used for persisting space identifiers.\n *\n * This key is used by SpaceIdStorage to store and retrieve space identifiers\n * from the browser's localStorage. The default value ensures consistency\n * across different parts of the application that need to access the stored space ID.\n */\nexport const DEFAULT_COSEC_SPACE_ID_KEY = 'cosec-space-id';\n\n/**\n * Configuration options for SpaceIdStorage.\n *\n * This interface extends KeyStorageOptions to provide SpaceId-specific\n * configuration while allowing customization of the underlying storage behavior.\n * All properties from KeyStorageOptions are optional and will use sensible defaults.\n *\n * @remarks\n * When no options are provided, SpaceIdStorage will use:\n * - DEFAULT_COSEC_SPACE_ID_KEY as the storage key\n * - A BroadcastTypedEventBus for cross-tab synchronization\n * - The browser's localStorage for persistence\n * - The identity serializer for direct string storage\n *\n * @example\n * ```typescript\n * // Default configuration\n * const storage = new SpaceIdStorage();\n *\n * // Custom key\n * const customStorage = new SpaceIdStorage({\n * key: 'my-custom-space-key'\n * });\n *\n * // Custom storage with event bus\n * const storageWithCustomBus = new SpaceIdStorage({\n * key: 'space-id',\n * eventBus: myCustomEventBus,\n * storage: myCustomStorage\n * });\n * ```\n */\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface SpaceIdStorageOptions extends Partial<\n KeyStorageOptions<SpaceId>\n> {}\n\n/**\n * Storage class for managing space identifiers with persistence and cross-tab synchronization.\n *\n * SpaceIdStorage extends KeyStorage to provide specialized storage for space\n * identifiers. It handles persistence to localStorage and provides automatic\n * synchronization across multiple browser tabs using the BroadcastChannel API.\n *\n * @remarks\n * Key features:\n * - Automatic persistence to localStorage\n * - Cross-tab synchronization via BroadcastChannel\n * - Caching for performance optimization\n * - Event-based notification of changes\n *\n * The storage uses the identity serializer, meaning space IDs are stored\n * and retrieved as-is without any transformation.\n *\n * @example\n * ```typescript\n * // Basic usage\n * const spaceStorage = new SpaceIdStorage();\n * spaceStorage.set('workspace-alpha');\n * const spaceId = spaceStorage.get(); // 'workspace-alpha'\n *\n * // Listening for changes\n * const removeListener = spaceStorage.addListener({\n * name: 'space-change',\n * handle: (event) => {\n * console.log('Space changed from', event.oldValue, 'to', event.newValue);\n * }\n * });\n *\n * // Cleanup\n * spaceStorage.destroy();\n * removeListener();\n * ```\n */\nexport class SpaceIdStorage extends KeyStorage<SpaceId> {\n /**\n * Creates a new SpaceIdStorage instance.\n *\n * @param options - Optional configuration options for the storage\n * @param options.key - The storage key (defaults to DEFAULT_COSEC_SPACE_ID_KEY)\n * @param options.eventBus - Custom event bus for cross-tab communication\n * @param options.storage - Custom storage implementation (defaults to localStorage)\n * @param options.serializer - Custom serializer (defaults to identity serializer)\n * @param options.defaultValue - Default value when no space ID is stored\n *\n * @throws Error if the underlying storage is unavailable\n *\n * @example\n * ```typescript\n * // Default configuration\n * const storage = new SpaceIdStorage();\n *\n * // With custom options\n * const storage = new SpaceIdStorage({\n * key: 'custom-space-key',\n * storage: sessionStorage,\n * defaultValue: 'default-space'\n * });\n * ```\n */\n constructor({\n key = DEFAULT_COSEC_SPACE_ID_KEY,\n eventBus = new BroadcastTypedEventBus({\n delegate: new SerialTypedEventBus(DEFAULT_COSEC_SPACE_ID_KEY),\n }),\n ...reset\n }: SpaceIdStorageOptions = {}) {\n super({ key, eventBus, ...reset, serializer: typedIdentitySerializer() });\n }\n}\n\n/**\n * Interface for predicates that determine if a resource requires space scoping.\n *\n * A SpacedResourcePredicate is used to evaluate whether a given request exchange\n * should be associated with a space identifier. This allows fine-grained control\n * over which resources belong to spaces and require space-based access control.\n *\n * @remarks\n * Predicates can implement various strategies:\n * - URL pattern matching (e.g., /spaces/{spaceId}/resources)\n * - Header presence checks\n * - Method-based filtering\n * - Custom business logic\n *\n * @example\n * ```typescript\n * // URL-based predicate for space-scoped resources\n * const urlPredicate: SpacedResourcePredicate = {\n * test: (exchange) => {\n * return exchange.request.url.includes('/spaces/');\n * }\n * };\n *\n * // Header-based predicate\n * const headerPredicate: SpacedResourcePredicate = {\n * test: (exchange) => {\n * return 'X-Require-Space' in exchange.request.headers;\n * }\n * };\n *\n * // Composite predicate using logical operations\n * const compositePredicate: SpacedResourcePredicate = {\n * test: (exchange) => {\n * return urlPredicate.test(exchange) && headerPredicate.test(exchange);\n * }\n * };\n * ```\n */\nexport interface SpacedResourcePredicate {\n /**\n * Tests whether the given exchange represents a space-scoped resource.\n *\n * This method evaluates the request exchange against the predicate's criteria\n * to determine if space-based routing or access control should be applied.\n *\n * @param exchange - The fetch exchange to evaluate\n * @returns true if the resource requires space scoping, false otherwise\n *\n * @example\n * ```typescript\n * const predicate: SpacedResourcePredicate = {\n * test: (exchange) => {\n * // Only resources under /api/spaces/ require space identification\n * return exchange.request.url.startsWith('/api/v1/spaces/');\n * }\n * };\n * ```\n */\n test(exchange: FetchExchange): boolean;\n}\n\n/**\n * Configuration options for DefaultSpaceIdProvider.\n *\n * This interface combines the predicate and storage dependencies required\n * by DefaultSpaceIdProvider to resolve space identifiers.\n */\nexport interface SpaceIdProviderOptions {\n /**\n * The predicate used to determine if a request requires space scoping.\n *\n * This predicate is consulted before attempting to retrieve the space ID\n * from storage. If the predicate returns false, no space ID will be returned.\n */\n spacedResourcePredicate: SpacedResourcePredicate;\n\n /**\n * The storage instance for persisting and retrieving space identifiers.\n *\n * This storage is used to look up the space ID once the predicate\n * confirms that the request requires space scoping.\n */\n spaceIdStorage: SpaceIdStorage;\n}\n\n/**\n * Default implementation of SpaceIdProvider that combines predicate-based\n * filtering with persistent storage.\n *\n * DefaultSpaceIdProvider implements a two-step resolution strategy:\n * 1. First, the predicate determines if the request requires space scoping\n * 2. If yes, the space ID is retrieved from the configured storage\n *\n * This separation enables:\n * - Efficient filtering of non-spaced resources\n * - Flexible predicate logic without storage coupling\n * - Testable and replaceable components\n *\n * @example\n * ```typescript\n * // Create storage for space IDs\n * const spaceStorage = new SpaceIdStorage();\n *\n * // Create predicate for space-scoped resources\n * const spacedResourcePredicate: SpacedResourcePredicate = {\n * test: (exchange) => {\n * return exchange.request.url.includes('/spaces/');\n * }\n * };\n *\n * // Create the provider\n * const spaceIdProvider = new DefaultSpaceIdProvider({\n * spacedResourcePredicate,\n * spaceIdStorage: spaceStorage\n * });\n *\n * // Use with CoSecRequestInterceptor\n * const interceptor = new CoSecRequestInterceptor({\n * appId: 'my-app',\n * deviceIdStorage,\n * spaceIdProvider\n * });\n * ```\n */\nexport class DefaultSpaceIdProvider implements SpaceIdProvider {\n /**\n * Creates a new DefaultSpaceIdProvider instance.\n *\n * @param options - The configuration options containing the predicate and storage\n * @param options.spacedResourcePredicate - Determines which requests require space scoping\n * @param options.spaceIdStorage - Provides access to the persisted space identifier\n *\n * @example\n * ```typescript\n * const provider = new DefaultSpaceIdProvider({\n * spacedResourcePredicate: {\n * test: (exchange) => exchange.request.url.includes('/api/')\n * },\n * spaceIdStorage: new SpaceIdStorage()\n * });\n * ```\n */\n constructor(private options: SpaceIdProviderOptions) {}\n\n /**\n * Resolves the space identifier for a given fetch exchange.\n *\n * This method first checks if the request requires space scoping by\n * evaluating the predicate. If the predicate returns true, it retrieves\n * the space ID from storage. Otherwise, it returns null.\n *\n * @param exchange - The fetch exchange containing the HTTP request details\n * @returns The space identifier if the request requires scoping and one exists,\n * otherwise null\n *\n * @example\n * ```typescript\n * const provider = new DefaultSpaceIdProvider({\n * spacedResourcePredicate: {\n * test: (exchange) => exchange.request.url.includes('/spaced/')\n * },\n * spaceIdStorage: new SpaceIdStorage()\n * });\n *\n * // For a request to /api/spaces/workspace-alpha/resources\n * const exchange = createMockExchange({ url: '/api/spaces/workspace-alpha/resources' });\n * const spaceId = provider.resolveSpaceId(exchange); // Returns stored space ID\n *\n * // For a request to /api/public/resources\n * const publicExchange = createMockExchange({ url: '/api/public/resources' });\n * const noSpace = provider.resolveSpaceId(publicExchange); // Returns null\n * ```\n */\n resolveSpaceId(exchange: FetchExchange): SpaceId | null {\n if (!this.options.spacedResourcePredicate.test(exchange)) {\n return null;\n }\n return this.options.spaceIdStorage.get();\n }\n}\n","/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n DEFAULT_INTERCEPTOR_ORDER_STEP,\n FetchExchange,\n type RequestInterceptor,\n} from '@ahoo-wang/fetcher';\nimport { AppIdCapable, CoSecHeaders, DeviceIdStorageCapable } from './types';\nimport { idGenerator } from './idGenerator';\nimport { NoneSpaceIdProvider, SpaceIdProvider } from './spaceIdProvider';\nimport { DeviceIdStorage } from './deviceIdStorage';\n\n/**\n * Configuration options for CoSecRequestInterceptor.\n *\n * This interface combines the required dependencies (appId and deviceIdStorage)\n * with an optional spaceIdProvider for multi-tenant, multi-space applications.\n *\n * @remarks\n * The appId identifies your application in the CoSec authentication system.\n * The deviceIdStorage manages persistent device identifiers for tracking.\n * The spaceIdProvider (optional) enables space-scoped resource access control.\n *\n * @example\n * ```typescript\n * // Basic configuration\n * const options: CoSecRequestOptions = {\n * appId: 'my-web-app',\n * deviceIdStorage: new DeviceIdStorage()\n * };\n *\n * // With space support for multi-tenant applications\n * const optionsWithSpace: CoSecRequestOptions = {\n * appId: 'my-web-app',\n * deviceIdStorage: new DeviceIdStorage(),\n * spaceIdProvider: new DefaultSpaceIdProvider({\n * spacedResourcePredicate: urlPredicate,\n * spaceIdStorage: new SpaceIdStorage()\n * })\n * };\n * ```\n */\nexport interface CoSecRequestOptions\n extends AppIdCapable, DeviceIdStorageCapable {\n /**\n * Optional provider for resolving space identifiers from requests.\n *\n * When provided, enables space-scoped resource access by adding the\n * CoSec-Space-Id header to requests for space-scoped resources.\n *\n * @defaultValue NoneSpaceIdProvider (no space identification)\n */\n spaceIdProvider?: SpaceIdProvider;\n}\n\n/**\n * The unique identifier name for the CoSecRequestInterceptor.\n *\n * This constant is used by the Fetcher interceptor registration system\n * to identify and manage this interceptor instance.\n *\n * @example\n * ```typescript\n * // Used internally by Fetcher for interceptor registration\n * fetcher.addInterceptor(new CoSecRequestInterceptor(options));\n * console.log(interceptor.name); // 'CoSecRequestInterceptor'\n * ```\n */\nexport const COSEC_REQUEST_INTERCEPTOR_NAME = 'CoSecRequestInterceptor';\n\n/**\n * The execution order for the CoSecRequestInterceptor.\n *\n * This value is calculated to ensure the interceptor runs after request body\n * processing but before the actual HTTP request is made. The order is set\n * to Number.MIN_SAFE_INTEGER + DEFAULT_INTERCEPTOR_ORDER_STEP to guarantee\n * early execution while maintaining safe integer boundaries.\n *\n * @remarks\n * - Position: After RequestBodyInterceptor\n * - Position: Before FetchInterceptor\n * - Value: Number.MIN_SAFE_INTEGER + 1000\n *\n * @example\n * ```typescript\n * const interceptor = new CoSecRequestInterceptor(options);\n * console.log(interceptor.order); // -9007199254740990\n * ```\n */\nexport const COSEC_REQUEST_INTERCEPTOR_ORDER =\n Number.MIN_SAFE_INTEGER + DEFAULT_INTERCEPTOR_ORDER_STEP;\n\n/**\n * Attribute key used to mark requests that should skip token refresh.\n *\n * When this attribute is set to true on a request exchange, the\n * AuthorizationRequestInterceptor will skip the automatic token refresh\n * for that specific request. This is useful for operations where\n * token refresh would cause issues, such as logout requests.\n *\n * @remarks\n * Set this attribute on the exchange before the AuthorizationRequestInterceptor runs:\n * ```typescript\n * exchange.attributes.set(IGNORE_REFRESH_TOKEN_ATTRIBUTE_KEY, true);\n * ```\n *\n * @example\n * ```typescript\n * // Skip refresh during logout\n * const logoutExchange = await fetcher.createRequest('/logout', {\n * method: 'POST'\n * }).getExchange();\n * logoutExchange.attributes.set(IGNORE_REFRESH_TOKEN_ATTRIBUTE_KEY, true);\n * await fetcher.fetch(logoutExchange);\n * ```\n */\nexport const IGNORE_REFRESH_TOKEN_ATTRIBUTE_KEY = 'Ignore-Refresh-Token';\n\n/**\n * Request interceptor that automatically adds CoSec authentication headers to outgoing requests.\n *\n * CoSecRequestInterceptor enriches each HTTP request with security-related headers\n * that enable the CoSec authentication system to:\n * - Track device identifiers for security monitoring\n * - Identify the application making the request\n * - Correlate requests for logging and analytics\n * - Scope requests to specific spaces in multi-tenant environments\n *\n * @remarks\n * **Header Injection:**\n * This interceptor injects the following headers into every outgoing request:\n * - `CoSec-App-Id`: Identifies your application\n * - `CoSec-Device-Id`: Unique device identifier (persisted or generated)\n * - `CoSec-Request-Id`: Unique identifier for this request (per-request)\n * - `CoSec-Space-Id`: Space identifier (when spaceIdProvider returns a value)\n *\n * **Execution Order:**\n * The interceptor runs at COSEC_REQUEST_INTERCEPTOR_ORDER position, which ensures:\n * 1. Request body processing is complete\n * 2. Authentication headers are not overwritten by subsequent interceptors\n * 3. Headers are present before the actual HTTP request is made\n *\n * **Device ID Management:**\n * - First request: Generates a new device ID using nanoid\n * - Subsequent requests: Reuses the stored device ID\n * - Storage: Persisted in localStorage with cross-tab synchronization\n *\n * **Space ID Resolution:**\n * - The spaceIdProvider determines if the request requires space scoping\n * - Space ID is resolved before header injection\n * - Supports both space-scoped and non-scoped resources\n *\n * @example\n * ```typescript\n * import { Fetcher } from '@ahoo-wang/fetcher';\n * import { CoSecRequestInterceptor } from '@ahoo-wang/fetcher-cosec';\n * import { DeviceIdStorage } from '@ahoo-wang/fetcher-cosec';\n *\n * // Create dependencies\n * const deviceIdStorage = new DeviceIdStorage();\n *\n * // Create the interceptor\n * const coSecInterceptor = new CoSecRequestInterceptor({\n * appId: 'my-saas-application',\n * deviceIdStorage\n * });\n *\n * // Register with Fetcher\n * const fetcher = new Fetcher()\n * .addInterceptor(coSecInterceptor)\n * .setBaseUrl('https://api.example.com');\n *\n * // All requests will now include CoSec headers\n * const response = await fetcher.get('/api/users');\n * // Request headers include:\n * // - CoSec-App-Id: my-saas-application\n * // - CoSec-Device-Id: <unique-device-id>\n * // - CoSec-Request-Id: <unique-request-id>\n * ```\n *\n * @example\n * ```typescript\n * // Multi-tenant with space support\n * import { DefaultSpaceIdProvider, SpaceIdStorage } from '@ahoo-wang/fetcher-cosec';\n *\n * const spaceStorage = new SpaceIdStorage();\n * const spaceIdProvider = new DefaultSpaceIdProvider({\n * spacedResourcePredicate: {\n * test: (exchange) => exchange.request.url.includes('/spaces/')\n * },\n * spaceIdStorage: spaceStorage\n * });\n *\n * const coSecInterceptor = new CoSecRequestInterceptor({\n * appId: 'my-saas-application',\n * deviceIdStorage,\n * spaceIdProvider\n * });\n * ```\n */\nexport class CoSecRequestInterceptor implements RequestInterceptor {\n /**\n * The unique name of this interceptor.\n *\n * Used by the Fetcher interceptor system for identification and ordering.\n */\n readonly name = COSEC_REQUEST_INTERCEPTOR_NAME;\n\n /**\n * The execution order of this interceptor.\n *\n * Set to COSEC_REQUEST_INTERCEPTOR_ORDER to ensure proper positioning\n * in the interceptor chain.\n */\n readonly order = COSEC_REQUEST_INTERCEPTOR_ORDER;\n\n /**\n * The application identifier.\n *\n * This value is injected into the CoSec-App-Id header for every request.\n * It identifies your application in the CoSec authentication system.\n */\n private readonly appId: string;\n\n /**\n * Storage for device identifiers.\n *\n * Provides persistent storage and retrieval of device identifiers\n * with cross-tab synchronization.\n */\n private readonly deviceIdStorage: DeviceIdStorage;\n\n /**\n * Provider for resolving space identifiers.\n *\n * Determines which space a request belongs to (if any) for\n * multi-tenant, multi-space applications.\n */\n private readonly spaceIdProvider: SpaceIdProvider;\n\n /**\n * Creates a new CoSecRequestInterceptor instance.\n *\n * @param options - The CoSec configuration options\n * @param options.appId - The application identifier for CoSec authentication\n * @param options.deviceIdStorage - Storage for device identifier management\n * @param options.spaceIdProvider - Optional provider for space identification\n *\n * @throws Error if appId is empty or not provided\n * @throws Error if deviceIdStorage is not provided\n *\n * @example\n * ```typescript\n * // Basic instantiation\n * const interceptor = new CoSecRequestInterceptor({\n * appId: 'my-web-app',\n * deviceIdStorage: new DeviceIdStorage()\n * });\n *\n * // With custom device ID storage\n * const customStorage = new DeviceIdStorage({\n * key: 'custom-device-key',\n * storage: sessionStorage\n * });\n * const interceptorWithCustom = new CoSecRequestInterceptor({\n * appId: 'my-web-app',\n * deviceIdStorage: customStorage\n * });\n *\n * // With space support\n * const spaceStorage = new SpaceIdStorage();\n * const spaceProvider = new DefaultSpaceIdProvider({\n * spacedResourcePredicate: { test: (e) => true },\n * spaceIdStorage: spaceStorage\n * });\n * const interceptorWithSpace = new CoSecRequestInterceptor({\n * appId: 'my-web-app',\n * deviceIdStorage,\n * spaceIdProvider: spaceProvider\n * });\n * ```\n */\n constructor({\n appId,\n deviceIdStorage,\n spaceIdProvider,\n }: CoSecRequestOptions) {\n this.appId = appId;\n this.deviceIdStorage = deviceIdStorage;\n this.spaceIdProvider = spaceIdProvider ?? NoneSpaceIdProvider;\n }\n\n /**\n * Intercepts outgoing requests to add CoSec authentication headers.\n *\n * This method is called by the Fetcher interceptor chain for each request.\n * It enriches the request with security headers before the HTTP request is made.\n *\n * **Header Injection Process:**\n * 1. Generates a unique request ID using the idGenerator\n * 2. Retrieves or creates a device ID from deviceIdStorage\n * 3. Resolves space ID from spaceIdProvider (if applicable)\n * 4. Adds all headers to the request\n *\n * @param exchange - The fetch exchange containing the request to process\n *\n * @remarks\n * **Header Values:**\n * - `CoSec-App-Id`: Always set to the configured appId\n * - `CoSec-Device-Id`: Retrieved from storage or newly generated\n * - `CoSec-Request-Id`: Unique per request (not persisted)\n * - `CoSec-Space-Id`: Set only if spaceIdProvider returns a value\n *\n * **Error Handling:**\n * - If deviceIdStorage.getOrCreate() throws, the error propagates\n * - If spaceIdProvider.resolveSpaceId() throws, the error propagates\n * - If ensureRequestHeaders() fails, the error propagates\n *\n * **Thread Safety:**\n * - The idGenerator is safe for concurrent use\n * - deviceIdStorage handles concurrent access internally\n * - Each request gets a unique request ID\n *\n * @example\n * ```typescript\n * // The interceptor is called automatically by Fetcher\n * const fetcher = new Fetcher()\n * .addInterceptor(new CoSecRequestInterceptor({\n * appId: 'my-app',\n * deviceIdStorage: new DeviceIdStorage()\n * }));\n *\n * // This request will have CoSec headers added\n * const response = await fetcher.get('/api/data');\n * ```\n *\n * @example\n * ```typescript\n * // Headers added to outgoing request:\n * // GET /api/users HTTP/1.1\n * // Host: api.example.com\n * // CoSec-App-Id: my-app\n * // CoSec-Device-Id: abc123xyz789\n * // CoSec-Request-Id: req_abc123def456\n * // CoSec-Space-Id: workspace-alpha (if space resolved)\n * ```\n */\n async intercept(exchange: FetchExchange) {\n // Generate a unique request ID for this request\n const requestId = idGenerator.generateId();\n\n // Get or create a device ID\n const deviceId = this.deviceIdStorage.getOrCreate();\n\n // Resolve space ID for multi-tenant support\n const spaceId = this.spaceIdProvider.resolveSpaceId(exchange);\n\n // Ensure request headers object exists\n const requestHeaders = exchange.ensureRequestHeaders();\n\n // Add CoSec headers to the request\n requestHeaders[CoSecHeaders.APP_ID] = this.appId;\n requestHeaders[CoSecHeaders.DEVICE_ID] = deviceId;\n requestHeaders[CoSecHeaders.REQUEST_ID] = requestId;\n if (spaceId) {\n requestHeaders[CoSecHeaders.SPACE_ID] = spaceId;\n }\n }\n}\n","/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n DEFAULT_INTERCEPTOR_ORDER_STEP,\n FetchExchange,\n RequestInterceptor,\n} from '@ahoo-wang/fetcher';\nimport {\n COSEC_REQUEST_INTERCEPTOR_ORDER,\n IGNORE_REFRESH_TOKEN_ATTRIBUTE_KEY,\n} from './cosecRequestInterceptor';\nimport { CoSecHeaders, JwtTokenManagerCapable } from './types';\n\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface AuthorizationInterceptorOptions\n extends JwtTokenManagerCapable {\n}\n\nexport const AUTHORIZATION_REQUEST_INTERCEPTOR_NAME =\n 'AuthorizationRequestInterceptor';\nexport const AUTHORIZATION_REQUEST_INTERCEPTOR_ORDER =\n COSEC_REQUEST_INTERCEPTOR_ORDER + DEFAULT_INTERCEPTOR_ORDER_STEP;\n\n/**\n * Request interceptor that automatically adds Authorization header to requests.\n *\n * This interceptor handles JWT token management by:\n * 1. Adding Authorization header with Bearer token if not already present\n * 2. Refreshing tokens when needed and possible\n * 3. Skipping refresh when explicitly requested via attributes\n *\n * The interceptor runs after CoSecRequestInterceptor but before FetchInterceptor in the chain.\n */\nexport class AuthorizationRequestInterceptor implements RequestInterceptor {\n readonly name = AUTHORIZATION_REQUEST_INTERCEPTOR_NAME;\n readonly order = AUTHORIZATION_REQUEST_INTERCEPTOR_ORDER;\n\n /**\n * Creates an AuthorizationRequestInterceptor instance.\n *\n * @param options - Configuration options containing the token manager\n */\n constructor(private readonly options: AuthorizationInterceptorOptions) {\n }\n\n /**\n * Intercepts the request exchange to add authorization headers.\n *\n * This method performs the following operations:\n * 1. Checks if a token exists and if Authorization header is already set\n * 2. Refreshes the token if needed, possible, and not explicitly ignored\n * 3. Adds the Authorization header with Bearer token if a token is available\n *\n * @param exchange - The fetch exchange containing request information\n * @returns Promise that resolves when the interception is complete\n */\n async intercept(exchange: FetchExchange): Promise<void> {\n // Get the current token from token manager\n let currentToken = this.options.tokenManager.currentToken;\n\n const requestHeaders = exchange.ensureRequestHeaders();\n\n // Skip if no token exists or Authorization header is already set\n if (!currentToken || requestHeaders[CoSecHeaders.AUTHORIZATION]) {\n return;\n }\n\n // Refresh token if needed and refreshable\n if (\n !exchange.attributes.has(IGNORE_REFRESH_TOKEN_ATTRIBUTE_KEY) &&\n currentToken.isRefreshNeeded &&\n currentToken.isRefreshable\n ) {\n await this.options.tokenManager.refresh();\n }\n\n // Get the current token again (might have been refreshed)\n currentToken = this.options.tokenManager.currentToken;\n\n // Add Authorization header if we have a token\n if (currentToken) {\n requestHeaders[CoSecHeaders.AUTHORIZATION] =\n `Bearer ${currentToken.access.token}`;\n }\n }\n}\n","/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { ResponseCodes } from './types';\nimport { FetchExchange, type ResponseInterceptor } from '@ahoo-wang/fetcher';\nimport { AuthorizationInterceptorOptions } from './authorizationRequestInterceptor';\n\n/**\n * The name of the AuthorizationResponseInterceptor.\n */\nexport const AUTHORIZATION_RESPONSE_INTERCEPTOR_NAME =\n 'AuthorizationResponseInterceptor';\n\n/**\n * The order of the AuthorizationResponseInterceptor.\n * Set to a high negative value to ensure it runs early in the interceptor chain.\n */\nexport const AUTHORIZATION_RESPONSE_INTERCEPTOR_ORDER =\n Number.MIN_SAFE_INTEGER + 1000;\n\n/**\n * CoSecResponseInterceptor is responsible for handling unauthorized responses (401)\n * by attempting to refresh the authentication token and retrying the original request.\n *\n * This interceptor:\n * 1. Checks if the response status is 401 (UNAUTHORIZED)\n * 2. If so, and if there's a current token, attempts to refresh it\n * 3. On successful refresh, stores the new token and retries the original request\n * 4. On refresh failure, clears stored tokens and propagates the error\n */\nexport class AuthorizationResponseInterceptor implements ResponseInterceptor {\n readonly name = AUTHORIZATION_RESPONSE_INTERCEPTOR_NAME;\n readonly order = AUTHORIZATION_RESPONSE_INTERCEPTOR_ORDER;\n\n /**\n * Creates a new AuthorizationResponseInterceptor instance.\n * @param options - The CoSec configuration options including token storage and refresher\n */\n constructor(private options: AuthorizationInterceptorOptions) {}\n\n /**\n * Intercepts the response and handles unauthorized responses by refreshing tokens.\n * @param exchange - The fetch exchange containing request and response information\n */\n async intercept(exchange: FetchExchange): Promise<void> {\n const response = exchange.response;\n // If there's no response, nothing to intercept\n if (!response) {\n return;\n }\n\n // Only handle unauthorized responses (401)\n if (response.status !== ResponseCodes.UNAUTHORIZED) {\n return;\n }\n\n if (!this.options.tokenManager.isRefreshable) {\n return;\n }\n try {\n await this.options.tokenManager.refresh();\n // Retry the original request with the new token\n await exchange.fetcher.interceptors.exchange(exchange);\n } catch (error) {\n // If token refresh fails, clear stored tokens and re-throw the error\n this.options.tokenManager.tokenStorage.remove();\n throw error;\n }\n }\n}\n","/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { idGenerator } from './idGenerator';\nimport { KeyStorage, KeyStorageOptions, typedIdentitySerializer } from '@ahoo-wang/fetcher-storage';\nimport {\n BroadcastTypedEventBus,\n SerialTypedEventBus,\n} from '@ahoo-wang/fetcher-eventbus';\n\nexport const DEFAULT_COSEC_DEVICE_ID_KEY = 'cosec-device-id';\n\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface DeviceIdStorageOptions\n extends Partial<KeyStorageOptions<string>> {\n}\n\n/**\n * Storage class for managing device identifiers.\n */\nexport class DeviceIdStorage extends KeyStorage<string> {\n constructor({\n key = DEFAULT_COSEC_DEVICE_ID_KEY,\n eventBus = new BroadcastTypedEventBus({\n delegate: new SerialTypedEventBus(DEFAULT_COSEC_DEVICE_ID_KEY),\n }),\n ...reset\n }: DeviceIdStorageOptions = {}) {\n super({ key, eventBus, ...reset, serializer: typedIdentitySerializer() });\n }\n\n /**\n * Generate a new device ID.\n *\n * @returns A newly generated device ID\n */\n generateDeviceId(): string {\n return idGenerator.generateId();\n }\n\n /**\n * Get or create a device ID.\n *\n * @returns The existing device ID if available, otherwise a newly generated one\n */\n getOrCreate(): string {\n // Try to get existing device ID from storage\n let deviceId = this.get();\n if (!deviceId) {\n // Generate a new device ID and store it\n deviceId = this.generateDeviceId();\n this.set(deviceId);\n }\n\n return deviceId;\n }\n}\n","/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { ErrorInterceptor, FetchExchange } from '@ahoo-wang/fetcher';\nimport { ResponseCodes } from './types';\n\n/**\n * The name identifier for the ForbiddenErrorInterceptor.\n * Used for interceptor registration and identification in the interceptor chain.\n */\nexport const FORBIDDEN_ERROR_INTERCEPTOR_NAME = 'ForbiddenErrorInterceptor';\n\n/**\n * The execution order for the ForbiddenErrorInterceptor.\n * Set to 0, indicating it runs at default priority in the interceptor chain.\n */\nexport const FORBIDDEN_ERROR_INTERCEPTOR_ORDER = 0;\n\n/**\n * Configuration options for the ForbiddenErrorInterceptor.\n */\nexport interface ForbiddenErrorInterceptorOptions {\n /**\n * Callback function invoked when a forbidden (403) response is detected.\n * This allows custom handling of authorization failures, such as displaying\n * permission error messages, redirecting to appropriate pages, or triggering\n * privilege escalation flows.\n *\n * @param exchange - The fetch exchange containing the request and response details\n * that resulted in the forbidden error\n * @returns Promise that resolves when the forbidden error handling is complete\n *\n * @example\n * ```typescript\n * const options: ForbiddenErrorInterceptorOptions = {\n * onForbidden: async (exchange) => {\n * console.log('Access forbidden for:', exchange.request.url);\n * // Show permission error or redirect\n * showPermissionError('You do not have permission to access this resource');\n * }\n * };\n * ```\n */\n onForbidden: (exchange: FetchExchange) => Promise<void>;\n}\n\n/**\n * An error interceptor that handles HTTP 403 Forbidden responses by invoking a custom callback.\n *\n * This interceptor is designed to provide centralized handling of authorization failures\n * across all HTTP requests. When a response with status code 403 is encountered, it calls\n * the configured `onForbidden` callback, allowing applications to implement custom\n * authorization recovery logic such as:\n * - Displaying permission error messages\n * - Redirecting users to access request pages\n * - Triggering privilege escalation workflows\n * - Logging security events\n * - Showing upgrade prompts for premium features\n *\n * The interceptor does not modify the response or retry requests automatically - it delegates\n * all handling to the provided callback function. This allows for flexible, application-specific\n * handling of forbidden access scenarios.\n *\n * @example\n * ```typescript\n * // Basic usage with error display\n * const interceptor = new ForbiddenErrorInterceptor({\n * onForbidden: async (exchange) => {\n * console.log('Forbidden access detected for:', exchange.request.url);\n * showErrorToast('You do not have permission to access this resource');\n * }\n * });\n *\n * fetcher.interceptors.error.use(interceptor);\n * ```\n *\n * @example\n * ```typescript\n * // Advanced usage with role-based handling\n * const interceptor = new ForbiddenErrorInterceptor({\n * onForbidden: async (exchange) => {\n * const userRole = getCurrentUserRole();\n *\n * if (userRole === 'guest') {\n * // Redirect to login for guests\n * redirectToLogin(exchange.request.url);\n * } else if (userRole === 'user') {\n * // Show upgrade prompt for basic users\n * showUpgradePrompt('Upgrade to premium for access to this feature');\n * } else {\n * // Log security event for authenticated users\n * logSecurityEvent('Forbidden access attempt', {\n * url: exchange.request.url,\n * userId: getCurrentUserId(),\n * timestamp: new Date().toISOString()\n * });\n * showErrorToast('Access denied due to insufficient permissions');\n * }\n * }\n * });\n * ```\n */\nexport class ForbiddenErrorInterceptor implements ErrorInterceptor {\n /**\n * The unique name identifier for this interceptor instance.\n * Used for registration, debugging, and interceptor chain management.\n */\n readonly name = FORBIDDEN_ERROR_INTERCEPTOR_NAME;\n\n /**\n * The execution order priority for this interceptor in the error interceptor chain.\n * Lower values execute earlier in the chain. Default priority (0) allows other\n * interceptors to run first if needed.\n */\n readonly order = FORBIDDEN_ERROR_INTERCEPTOR_ORDER;\n\n /**\n * Creates a new ForbiddenErrorInterceptor instance.\n *\n * @param options - Configuration options containing the callback to handle forbidden responses.\n * Must include the `onForbidden` callback function.\n *\n * @throws Will throw an error if options are not provided or if `onForbidden` callback is missing.\n *\n * @example\n * ```typescript\n * const interceptor = new ForbiddenErrorInterceptor({\n * onForbidden: async (exchange) => {\n * // Handle forbidden access\n * }\n * });\n * ```\n */\n constructor(private options: ForbiddenErrorInterceptorOptions) {}\n\n /**\n * Intercepts fetch exchanges to detect and handle forbidden (403) responses.\n *\n * This method examines the response status code and invokes the configured `onForbidden`\n * callback when a 403 Forbidden response is detected. The method is asynchronous to\n * allow the callback to perform async operations like API calls, redirects, or UI updates.\n *\n * The interceptor only acts on responses with status code 403. Other error codes are\n * ignored and passed through to other error interceptors in the chain.\n *\n * @param exchange - The fetch exchange containing request, response, and error information\n * to be inspected for forbidden status codes. The exchange object provides\n * access to the original request, response details, and any error information.\n * @returns Promise that resolves when the forbidden error handling is complete.\n * Returns void - the method does not modify the exchange or return values.\n *\n * @remarks\n * - Only responds to HTTP 403 status codes\n * - Does not retry requests or modify responses\n * - Allows async operations in the callback\n * - Does not throw exceptions - delegates all error handling to the callback\n * - Safe to use with other error interceptors\n *\n * @example\n * ```typescript\n * // The intercept method is called automatically by the fetcher\n * // No manual invocation needed - this is for documentation purposes\n * const interceptor = new ForbiddenErrorInterceptor({\n * onForbidden: async (exchange) => {\n * // exchange.response.status === 403\n * // exchange.request contains original request details\n * await handleForbiddenAccess(exchange);\n * }\n * });\n * ```\n */\n async intercept(exchange: FetchExchange): Promise<void> {\n // Check if the response status indicates forbidden access (403)\n if (exchange.response?.status === ResponseCodes.FORBIDDEN) {\n // Invoke the custom forbidden error handler\n // Allow the callback to perform async operations\n await this.options.onForbidden(exchange);\n }\n }\n}\n","/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { TokenStorage } from './tokenStorage';\nimport { TokenRefresher } from './tokenRefresher';\nimport { JwtCompositeToken, RefreshTokenStatusCapable } from './jwtToken';\nimport { FetcherError } from '@ahoo-wang/fetcher';\n\nexport class RefreshTokenError extends FetcherError {\n constructor(\n public readonly token: JwtCompositeToken,\n cause?: Error | any,\n ) {\n super(`Refresh token failed.`, cause);\n this.name = 'RefreshTokenError';\n Object.setPrototypeOf(this, RefreshTokenError.prototype);\n }\n}\n\n/**\n * Manages JWT token refreshing operations and provides status information\n */\nexport class JwtTokenManager implements RefreshTokenStatusCapable {\n private refreshInProgress?: Promise<void>;\n\n /**\n * Creates a new JwtTokenManager instance\n * @param tokenStorage The storage used to persist tokens\n * @param tokenRefresher The refresher used to refresh expired tokens\n */\n constructor(\n public readonly tokenStorage: TokenStorage,\n public readonly tokenRefresher: TokenRefresher,\n ) {}\n\n /**\n * Gets the current JWT composite token from storage\n * @returns The current token or null if none exists\n */\n get currentToken(): JwtCompositeToken | null {\n return this.tokenStorage.get();\n }\n\n /**\n * Refreshes the JWT token\n * @returns Promise that resolves when refresh is complete\n * @throws Error if no token is found or refresh fails\n */\n async refresh(): Promise<void> {\n const jwtToken = this.currentToken;\n if (!jwtToken) {\n throw new Error('No token found');\n }\n if (this.refreshInProgress) {\n return this.refreshInProgress;\n }\n\n this.refreshInProgress = this.tokenRefresher\n .refresh(jwtToken.token)\n .then(newToken => {\n this.tokenStorage.setCompositeToken(newToken);\n })\n .catch(error => {\n this.tokenStorage.remove();\n throw new RefreshTokenError(jwtToken, error);\n })\n .finally(() => {\n this.refreshInProgress = undefined;\n });\n\n return this.refreshInProgress;\n }\n\n /**\n * Indicates if the current token needs to be refreshed\n * @returns true if the access token is expired and needs refresh, false otherwise\n */\n get isRefreshNeeded(): boolean {\n if (!this.currentToken) {\n return false;\n }\n return this.currentToken.isRefreshNeeded;\n }\n\n /**\n * Indicates if the current token can be refreshed\n * @returns true if the refresh token is still valid, false otherwise\n */\n get isRefreshable(): boolean {\n if (!this.currentToken) {\n return false;\n }\n return this.currentToken.isRefreshable;\n }\n}\n","/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n DEFAULT_INTERCEPTOR_ORDER_STEP,\n FetchExchange,\n RequestInterceptor, URL_RESOLVE_INTERCEPTOR_ORDER,\n} from '@ahoo-wang/fetcher';\nimport { TokenStorage } from './tokenStorage';\n\nconst TENANT_ID_PATH_KEY = 'tenantId';\nconst OWNER_ID_PATH_KEY = 'ownerId';\n\n/**\n * Configuration options for resource attribution\n */\nexport interface ResourceAttributionOptions {\n /**\n * The path parameter key used for tenant ID in URL templates\n */\n tenantId?: string;\n /**\n * The path parameter key used for owner ID in URL templates\n */\n ownerId?: string;\n /**\n * Storage mechanism for retrieving current authentication tokens\n */\n tokenStorage: TokenStorage;\n}\n\n/**\n * Name identifier for the ResourceAttributionRequestInterceptor\n */\nexport const RESOURCE_ATTRIBUTION_REQUEST_INTERCEPTOR_NAME =\n 'ResourceAttributionRequestInterceptor';\n/**\n * Order priority for the ResourceAttributionRequestInterceptor, set to maximum safe integer to ensure it runs last\n */\nexport const RESOURCE_ATTRIBUTION_REQUEST_INTERCEPTOR_ORDER =\n URL_RESOLVE_INTERCEPTOR_ORDER - DEFAULT_INTERCEPTOR_ORDER_STEP;\n\n/**\n * Request interceptor that automatically adds tenant and owner ID path parameters to requests\n * based on the current authentication token. This is useful for multi-tenant applications where\n * requests need to include tenant-specific information in the URL path.\n */\nexport class ResourceAttributionRequestInterceptor\n implements RequestInterceptor {\n readonly name = RESOURCE_ATTRIBUTION_REQUEST_INTERCEPTOR_NAME;\n readonly order = RESOURCE_ATTRIBUTION_REQUEST_INTERCEPTOR_ORDER;\n private readonly tenantIdPathKey: string;\n private readonly ownerIdPathKey: string;\n private readonly tokenStorage: TokenStorage;\n\n /**\n * Creates a new ResourceAttributionRequestInterceptor\n * @param options - Configuration options for resource attribution including tenantId, ownerId and tokenStorage\n */\n constructor({\n tenantId = TENANT_ID_PATH_KEY,\n ownerId = OWNER_ID_PATH_KEY,\n tokenStorage,\n }: ResourceAttributionOptions) {\n this.tenantIdPathKey = tenantId;\n this.ownerIdPathKey = ownerId;\n this.tokenStorage = tokenStorage;\n }\n\n /**\n * Intercepts outgoing requests and automatically adds tenant and owner ID path parameters\n * if they are defined in the URL template but not provided in the request.\n * @param exchange - The fetch exchange containing the request information\n */\n intercept(exchange: FetchExchange): void {\n const currentToken = this.tokenStorage.get();\n if (!currentToken) {\n return;\n }\n const principal = currentToken.access.payload;\n if (!principal) {\n return;\n }\n if (!principal.tenantId && !principal.sub) {\n return;\n }\n\n // Extract path parameters from the URL template\n const extractedPathParams =\n exchange.fetcher.urlBuilder.urlTemplateResolver.extractPathParams(\n exchange.request.url,\n );\n const tenantIdPathKey = this.tenantIdPathKey;\n const requestPathParams = exchange.ensureRequestUrlParams().path;\n const tenantId = principal.tenantId;\n\n // Add tenant ID to path parameters if it's part of the URL template and not already provided\n if (\n tenantId &&\n extractedPathParams.includes(tenantIdPathKey) &&\n !requestPathParams[tenantIdPathKey]\n ) {\n requestPathParams[tenantIdPathKey] = tenantId;\n }\n\n const ownerIdPathKey = this.ownerIdPathKey;\n const ownerId = principal.sub;\n\n // Add owner ID to path parameters if it's part of the URL template and not already provided\n if (\n ownerId &&\n extractedPathParams.includes(ownerIdPathKey) &&\n !requestPathParams[ownerIdPathKey]\n ) {\n requestPathParams[ownerIdPathKey] = ownerId;\n }\n }\n}\n","/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n/**\n * Interface representing a JWT payload as defined in RFC 7519.\n * Contains standard JWT claims as well as custom properties.\n */\nexport interface JwtPayload {\n /**\n * JWT ID - provides a unique identifier for the JWT.\n */\n jti: string;\n /**\n * Subject - identifies the principal that is the subject of the JWT.\n */\n sub: string;\n /**\n * Issuer - identifies the principal that issued the JWT.\n */\n iss?: string;\n /**\n * Audience - identifies the recipients that the JWT is intended for.\n * Can be a single string or an array of strings.\n */\n aud?: string | string[];\n /**\n * Expiration Time - identifies the expiration time on or after which the JWT MUST NOT be accepted for processing.\n * Represented as NumericDate (seconds since Unix epoch).\n */\n exp: number;\n /**\n * Not Before - identifies the time before which the JWT MUST NOT be accepted for processing.\n * Represented as NumericDate (seconds since Unix epoch).\n */\n nbf?: number;\n /**\n * Issued At - identifies the time at which the JWT was issued.\n * Represented as NumericDate (seconds since Unix epoch).\n */\n iat: number;\n\n /**\n * Allows additional custom properties to be included in the payload.\n */\n [key: string]: any;\n}\n\n/**\n * Interface representing a JWT payload with CoSec-specific extensions.\n * Extends the standard JwtPayload interface with additional CoSec-specific properties.\n */\nexport interface CoSecJwtPayload extends JwtPayload {\n /**\n * Tenant identifier - identifies the tenant scope for the JWT.\n */\n tenantId?: string;\n /**\n * Policies - array of policy identifiers associated with the JWT.\n * These are security policies defined internally by Cosec.\n */\n policies?: string[];\n /**\n * Roles - array of role identifiers associated with the JWT.\n * Role IDs indicate what roles the token belongs to.\n */\n roles?: string[];\n /**\n * Attributes - custom key-value pairs providing additional information about the JWT.\n */\n attributes?: Record<string, any>;\n}\n\n/**\n * Parses a JWT token and extracts its payload.\n *\n * This function decodes the payload part of a JWT token, handling Base64URL decoding\n * and JSON parsing. It validates the token structure and returns null for invalid tokens.\n *\n * @param token - The JWT token string to parse\n * @returns The parsed JWT payload or null if parsing fails\n */\nexport function parseJwtPayload<T extends JwtPayload>(token: string): T | null {\n try {\n if (typeof token !== 'string') {\n return null;\n }\n const parts = token.split('.');\n if (parts.length !== 3) {\n return null;\n }\n\n const base64Url = parts[1];\n const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');\n\n // Add padding if needed\n const paddedBase64 = base64.padEnd(\n base64.length + ((4 - (base64.length % 4)) % 4),\n '=',\n );\n\n const jsonPayload = decodeURIComponent(\n atob(paddedBase64)\n .split('')\n .map(function (c) {\n return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);\n })\n .join(''),\n );\n return JSON.parse(jsonPayload) as T;\n } catch (error) {\n // Avoid exposing sensitive information in error logs\n console.error('Failed to parse JWT token', error);\n return null;\n }\n}\n\nexport interface EarlyPeriodCapable {\n /**\n * The time in seconds before actual expiration when the token should be considered expired (default: 0)\n */\n readonly earlyPeriod: number;\n}\n\n/**\n * Checks if a JWT token is expired based on its expiration time (exp claim).\n *\n * This function determines if a JWT token has expired by comparing its exp claim\n * with the current time. If the token is a string, it will be parsed first.\n * Tokens without an exp claim are considered not expired.\n *\n * The early period parameter allows for early token expiration, which is useful\n * for triggering token refresh before the token actually expires. This helps\n * avoid race conditions where a token expires between the time it is checked and\n * the time it is used.\n *\n * @param token - The JWT token to check, either as a string or as a JwtPayload object\n * @param earlyPeriod - The time in seconds before actual expiration when the token should be considered expired (default: 0)\n * @returns true if the token is expired (or will expire within the early period) or cannot be parsed, false otherwise\n */\nexport function isTokenExpired(\n token: string | CoSecJwtPayload,\n earlyPeriod: number = 0,\n): boolean {\n const payload = typeof token === 'string' ? parseJwtPayload(token) : token;\n if (!payload) {\n return true;\n }\n\n const expAt = payload.exp;\n if (!expAt) {\n return false;\n }\n\n const now = Date.now() / 1000;\n return now > expAt - earlyPeriod;\n}\n","/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n CoSecJwtPayload,\n EarlyPeriodCapable,\n isTokenExpired,\n JwtPayload,\n parseJwtPayload,\n} from './jwts';\nimport { CompositeToken } from './tokenRefresher';\nimport { Serializer } from '@ahoo-wang/fetcher-storage';\n\n/**\n * Interface for JWT token with typed payload.\n *\n * This interface represents a JWT token that includes the raw token string,\n * a parsed payload of a specific type, and methods to check expiration status.\n * It extends EarlyPeriodCapable to support early expiration checks.\n *\n * @template Payload The type of the JWT payload, must extend JwtPayload\n *\n * @example\n * ```typescript\n * interface CustomPayload extends JwtPayload {\n * userId: string;\n * roles: string[];\n * }\n *\n * const token: IJwtToken<CustomPayload> = new JwtToken<CustomPayload>('jwt.token.here');\n * if (!token.isExpired) {\n * console.log(token.payload?.userId);\n * }\n * ```\n */\nexport interface IJwtToken<\n Payload extends JwtPayload,\n> extends EarlyPeriodCapable {\n /**\n * The raw JWT token string.\n */\n readonly token: string;\n\n /**\n * The parsed JWT payload. Null if the token could not be parsed.\n */\n readonly payload: Payload | null;\n\n /**\n * Indicates whether the token is expired, considering the early period.\n */\n isExpired: boolean;\n}\n\n/**\n * Class representing a JWT token with typed payload.\n *\n * This class provides a concrete implementation of IJwtToken, parsing the JWT\n * token string and providing access to the payload and expiration status.\n * It supports early expiration periods for proactive token refresh.\n *\n * @template Payload The type of the JWT payload, must extend JwtPayload\n *\n * @example\n * ```typescript\n * const token = new JwtToken<CoSecJwtPayload>('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...', 300000); // 5 min early period\n * console.log(token.isExpired); // false if not expired\n * console.log(token.payload?.sub); // user ID from payload\n * ```\n */\nexport class JwtToken<\n Payload extends JwtPayload,\n> implements IJwtToken<Payload> {\n /**\n * The parsed JWT payload. Null if the token could not be parsed.\n */\n public readonly payload: Payload | null;\n\n /**\n * Creates a new JwtToken instance.\n *\n * Parses the JWT token string to extract the payload and stores the early period\n * for expiration checks.\n *\n * @param token The raw JWT token string to parse\n * @param earlyPeriod The early expiration period in milliseconds (default: 0).\n * Tokens are considered expired this many milliseconds before their actual expiration time.\n *\n * @throws Will not throw but payload will be null if token parsing fails\n */\n constructor(\n public readonly token: string,\n public readonly earlyPeriod: number = 0,\n ) {\n this.payload = parseJwtPayload<Payload>(token);\n }\n\n /**\n * Checks if the token is expired.\n *\n * Considers both the token's expiration time and the early period.\n * Returns true if the payload is null (parsing failed) or if the token is expired.\n *\n * @returns true if the token is expired or invalid, false otherwise\n */\n get isExpired(): boolean {\n if (!this.payload) {\n return true;\n }\n return isTokenExpired(this.payload, this.earlyPeriod);\n }\n}\n\n/**\n * Interface for checking refresh token status capabilities.\n *\n * This interface defines methods to check if token refresh is needed and possible.\n */\nexport interface RefreshTokenStatusCapable {\n /**\n * Checks if the access token needs to be refreshed.\n *\n * @returns true if the access token is expired, false otherwise\n */\n readonly isRefreshNeeded: boolean;\n\n /**\n * Checks if the refresh token is still valid and can be used to refresh the access token.\n *\n * @returns true if the refresh token is not expired, false otherwise\n */\n readonly isRefreshable: boolean;\n}\n\n/**\n * Class representing a composite token containing both access and refresh tokens.\n *\n * This class manages both access and refresh JWT tokens together, providing\n * convenient methods to check authentication status, refresh needs, and user information.\n * It implements both EarlyPeriodCapable and RefreshTokenStatusCapable interfaces.\n *\n * @example\n * ```typescript\n * const compositeToken = new JwtCompositeToken({\n * accessToken: 'access.jwt.here',\n * refreshToken: 'refresh.jwt.here'\n * }, 300000); // 5 min early period\n *\n * if (compositeToken.authenticated) {\n * console.log(compositeToken.currentUser?.sub);\n * }\n *\n * if (compositeToken.isRefreshNeeded && compositeToken.isRefreshable) {\n * // Refresh the access token\n * }\n * ```\n */\nexport class JwtCompositeToken\n implements EarlyPeriodCapable, RefreshTokenStatusCapable {\n /**\n * The access JWT token instance.\n */\n public readonly access: JwtToken<CoSecJwtPayload>;\n\n /**\n * The refresh JWT token instance.\n */\n public readonly refresh: JwtToken<JwtPayload>;\n\n /**\n * Creates a new JwtCompositeToken instance.\n *\n * Initializes both access and refresh token instances with the provided early period.\n *\n * @param token The composite token containing access and refresh token strings\n * @param earlyPeriod The early expiration period in milliseconds (default: 0)\n */\n constructor(\n public readonly token: CompositeToken,\n public readonly earlyPeriod: number = 0,\n ) {\n this.access = new JwtToken(token.accessToken, earlyPeriod);\n this.refresh = new JwtToken(token.refreshToken, earlyPeriod);\n }\n\n /**\n * Checks if the access token needs to be refreshed.\n *\n * @returns true if the access token is expired, false otherwise\n */\n get isRefreshNeeded(): boolean {\n return this.access.isExpired;\n }\n\n /**\n * Checks if the refresh token is still valid and can be used to refresh the access token.\n *\n * @returns true if the refresh token is not expired, false otherwise\n */\n get isRefreshable(): boolean {\n return !this.refresh.isExpired;\n }\n\n /**\n * Checks if the user is currently authenticated (access token is valid).\n *\n * @returns true if the access token is not expired, false otherwise\n */\n get authenticated(): boolean {\n return !this.access.isExpired;\n }\n}\n\n/**\n * Serializer for JwtCompositeToken that handles conversion to and from JSON strings.\n *\n * This class provides serialization and deserialization functionality for JwtCompositeToken\n * instances, allowing them to be stored and retrieved from persistent storage.\n *\n * @example\n * ```typescript\n * const serializer = new JwtCompositeTokenSerializer(300000);\n * const token = new JwtCompositeToken({ accessToken: '...', refreshToken: '...' });\n *\n * const serialized = serializer.serialize(token);\n * const deserialized = serializer.deserialize(serialized);\n * ```\n */\nexport class JwtCompositeTokenSerializer\n implements Serializer<string, JwtCompositeToken>, EarlyPeriodCapable {\n /**\n * Creates a new JwtCompositeTokenSerializer instance.\n *\n * @param earlyPeriod The early expiration period in milliseconds to use for deserialized tokens (default: 0)\n */\n constructor(public readonly earlyPeriod: number = 0) {\n }\n\n /**\n * Deserializes a JSON string to a JwtCompositeToken.\n *\n * Parses the JSON string and creates a new JwtCompositeToken instance with the stored tokens.\n *\n * @param value The JSON string representation of a composite token\n * @returns A JwtCompositeToken instance\n * @throws SyntaxError if the JSON string is invalid\n * @throws Error if the parsed object doesn't match the expected CompositeToken structure\n */\n deserialize(value: string): JwtCompositeToken {\n const compositeToken = JSON.parse(value) as CompositeToken;\n return new JwtCompositeToken(compositeToken, this.earlyPeriod);\n }\n\n /**\n * Serializes a JwtCompositeToken to a JSON string.\n *\n * Converts the composite token to a JSON string for storage.\n *\n * @param value The JwtCompositeToken to serialize\n * @returns A JSON string representation of the composite token\n */\n serialize(value: JwtCompositeToken): string {\n return JSON.stringify(value.token);\n }\n}\n\n/**\n * Default instance of JwtCompositeTokenSerializer with no early period.\n *\n * This pre-configured serializer can be used directly for basic serialization needs.\n */\nexport const jwtCompositeTokenSerializer = new JwtCompositeTokenSerializer();\n","/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { JwtCompositeToken, JwtCompositeTokenSerializer } from './jwtToken';\nimport { CompositeToken } from './tokenRefresher';\nimport { CoSecJwtPayload, EarlyPeriodCapable } from './jwts';\nimport { KeyStorage, KeyStorageOptions } from '@ahoo-wang/fetcher-storage';\nimport {\n BroadcastTypedEventBus,\n SerialTypedEventBus,\n} from '@ahoo-wang/fetcher-eventbus';\n\n/**\n * Default key used for storing CoSec tokens in storage.\n */\nexport const DEFAULT_COSEC_TOKEN_KEY = 'cosec-token';\n\n/**\n * Options for configuring TokenStorage.\n * Extends KeyStorageOptions excluding 'serializer' and includes EarlyPeriodCapable properties.\n */\nexport interface TokenStorageOptions\n extends\n Partial<Omit<KeyStorageOptions<JwtCompositeToken>, 'serializer'>>,\n Partial<EarlyPeriodCapable> {}\n\n/**\n * Storage class for managing access and refresh tokens in CoSec authentication.\n * Provides methods to store, retrieve, and manage JWT composite tokens with early period support.\n * Extends KeyStorage to handle persistence and implements EarlyPeriodCapable for token refresh timing.\n */\nexport class TokenStorage\n extends KeyStorage<JwtCompositeToken>\n implements EarlyPeriodCapable\n{\n /**\n * The early period in milliseconds for token refresh timing.\n */\n public readonly earlyPeriod: number;\n\n /**\n * Creates a new TokenStorage instance.\n * @param options - Configuration options for the token storage.\n * @param options.key - The storage key for tokens. Defaults to DEFAULT_COSEC_TOKEN_KEY.\n * @param options.eventBus - Event bus for token change notifications. Defaults to a BroadcastTypedEventBus with SerialTypedEventBus delegate.\n * @param options.earlyPeriod - Early period for token refresh in milliseconds. Defaults to 0.\n * @param reset - Additional options passed to KeyStorage.\n */\n constructor({\n key = DEFAULT_COSEC_TOKEN_KEY,\n eventBus = new BroadcastTypedEventBus({\n delegate: new SerialTypedEventBus(DEFAULT_COSEC_TOKEN_KEY),\n }),\n earlyPeriod = 0,\n ...reset\n }: TokenStorageOptions = {}) {\n super({\n key,\n eventBus,\n ...reset,\n serializer: new JwtCompositeTokenSerializer(earlyPeriod),\n });\n this.earlyPeriod = earlyPeriod;\n }\n\n /**\n * Sets a composite token in storage.\n * Converts the composite token to a JwtCompositeToken and stores it.\n * @deprecated Use signIn() instead for better semantic clarity.\n * @param compositeToken - The composite token containing access and refresh tokens.\n */\n setCompositeToken(compositeToken: CompositeToken) {\n this.signIn(compositeToken);\n }\n\n /**\n * Signs in by storing the composite token.\n * @param compositeToken - The composite token to store for authentication.\n */\n signIn(compositeToken: CompositeToken): void {\n this.set(new JwtCompositeToken(compositeToken, this.earlyPeriod));\n }\n\n /**\n * Signs out by removing the stored token.\n * Clears the token from storage.\n */\n signOut(): void {\n this.remove();\n }\n\n /**\n * Checks if the user is authenticated.\n * @returns true if a valid token is present and authenticated, false otherwise.\n */\n get authenticated(): boolean {\n return this.get()?.authenticated === true;\n }\n\n /**\n * Gets the current user's JWT payload.\n * @returns The JWT payload of the current user if authenticated, null otherwise.\n */\n get currentUser(): CoSecJwtPayload | null {\n if (!this.authenticated) {\n return null;\n }\n return this.get()?.access.payload ?? null;\n }\n}\n","/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { ErrorInterceptor, FetchExchange } from '@ahoo-wang/fetcher';\nimport { ResponseCodes } from './types';\nimport { RefreshTokenError } from './jwtTokenManager';\n\n/**\n * The name identifier for the UnauthorizedErrorInterceptor.\n * Used for interceptor registration and identification in the interceptor chain.\n */\nexport const UNAUTHORIZED_ERROR_INTERCEPTOR_NAME =\n 'UnauthorizedErrorInterceptor';\n\n/**\n * The execution order for the UnauthorizedErrorInterceptor.\n * Set to 0, indicating it runs at default priority in the interceptor chain.\n */\nexport const UNAUTHORIZED_ERROR_INTERCEPTOR_ORDER = 0;\n\n/**\n * Configuration options for the UnauthorizedErrorInterceptor.\n */\nexport interface UnauthorizedErrorInterceptorOptions {\n /**\n * Callback function invoked when an unauthorized (401) response is detected.\n * This allows custom handling of authentication failures, such as redirecting to login\n * or triggering token refresh mechanisms.\n *\n * @param exchange - The fetch exchange containing the request and response details\n * that resulted in the unauthorized error\n */\n onUnauthorized: (exchange: FetchExchange) => Promise<void> | void;\n}\n\n/**\n * An error interceptor that handles HTTP 401 Unauthorized responses by invoking a custom callback.\n *\n * This interceptor is designed to provide centralized handling of authentication failures\n * across all HTTP requests. When a response with status code 401 is encountered, it calls\n * the configured `onUnauthorized` callback, allowing applications to implement custom\n * authentication recovery logic such as:\n * - Redirecting users to login pages\n * - Triggering token refresh flows\n * - Clearing stored authentication state\n * - Displaying authentication error messages\n *\n * The interceptor does not modify the response or retry requests automatically - it delegates\n * all handling to the provided callback function.\n *\n * @example\n * ```typescript\n * const interceptor = new UnauthorizedErrorInterceptor({\n * onUnauthorized: (exchange) => {\n * console.log('Unauthorized access detected for:', exchange.request.url);\n * // Redirect to login page or refresh token\n * window.location.href = '/login';\n * }\n * });\n *\n * fetcher.interceptors.error.use(interceptor);\n * ```\n */\nexport class UnauthorizedErrorInterceptor implements ErrorInterceptor {\n /**\n * The unique name identifier for this interceptor instance.\n */\n readonly name = UNAUTHORIZED_ERROR_INTERCEPTOR_NAME;\n\n /**\n * The execution order priority for this interceptor in the chain.\n */\n readonly order = UNAUTHORIZED_ERROR_INTERCEPTOR_ORDER;\n\n /**\n * Creates a new UnauthorizedErrorInterceptor instance.\n *\n * @param options - Configuration options containing the callback to handle unauthorized responses\n */\n constructor(private options: UnauthorizedErrorInterceptorOptions) {}\n\n /**\n * Intercepts fetch exchanges to detect and handle unauthorized (401) responses\n * and RefreshTokenError exceptions.\n *\n * This method checks if the response status is 401 (Unauthorized) or if the exchange\n * contains an error of type `RefreshTokenError`. If either condition is met, it invokes\n * the configured `onUnauthorized` callback with the exchange details. The method\n * does not return a value or throw exceptions - all error handling is delegated\n * to the callback function.\n *\n * @param exchange - The fetch exchange containing request, response, and error information\n * to be inspected for unauthorized status codes or refresh token errors\n * @returns {void} This method does not return a value\n *\n * @example\n * ```typescript\n * const interceptor = new UnauthorizedErrorInterceptor({\n * onUnauthorized: (exchange) => {\n * // Custom logic here\n * }\n * });\n * ```\n */\n async intercept(exchange: FetchExchange): Promise<void> {\n if (\n exchange.response?.status === ResponseCodes.UNAUTHORIZED ||\n exchange.error instanceof RefreshTokenError\n ) {\n await this.options.onUnauthorized(exchange);\n }\n }\n}\n","/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { Fetcher, FetcherConfigurer, FetchExchange } from '@ahoo-wang/fetcher';\nimport { AuthorizationRequestInterceptor } from './authorizationRequestInterceptor';\nimport { AuthorizationResponseInterceptor } from './authorizationResponseInterceptor';\nimport { CoSecRequestInterceptor } from './cosecRequestInterceptor';\nimport { DeviceIdStorage } from './deviceIdStorage';\nimport { ForbiddenErrorInterceptor } from './forbiddenErrorInterceptor';\nimport { JwtTokenManager } from './jwtTokenManager';\nimport { ResourceAttributionRequestInterceptor } from './resourceAttributionRequestInterceptor';\nimport { TokenRefresher } from './tokenRefresher';\nimport { TokenStorage } from './tokenStorage';\nimport { UnauthorizedErrorInterceptor } from './unauthorizedErrorInterceptor';\nimport { AppIdCapable, DeviceIdStorageCapable } from './types';\nimport { NoneSpaceIdProvider, SpaceIdProvider } from './spaceIdProvider';\n\n/**\n * Configuration interface for CoSec security features.\n *\n * This interface provides a simplified, flexible configuration for setting up\n * CoSec authentication and security features. It supports both full JWT\n * authentication setups and minimal header injection configurations.\n *\n * @remarks\n * **Required Properties:**\n * - appId: Your application's unique identifier in the CoSec system\n *\n * **Optional Properties:**\n * - tokenStorage: Custom token persistence (defaults to TokenStorage)\n * - deviceIdStorage: Custom device ID management (defaults to DeviceIdStorage)\n * - tokenRefresher: JWT token refresh handler (enables authentication)\n * - spaceIdProvider: Space identifier resolver (enables multi-tenant)\n * - onUnauthorized: Custom 401 error handler\n * - onForbidden: Custom 403 error handler\n *\n * **Configuration Levels:**\n * - Minimal: Only appId (headers only, no authentication)\n * - Standard: appId + tokenRefresher (full JWT auth)\n * - Enterprise: All options (multi-tenant with custom handlers)\n *\n * @example\n * ```typescript\n * // Minimal: Headers only, no authentication\n * const minimalConfig: CoSecConfig = {\n * appId: 'my-web-app'\n * };\n *\n * // Standard: Full JWT authentication\n * const standardConfig: CoSecConfig = {\n * appId: 'my-web-app',\n * tokenRefresher: {\n * refresh: async (token) => refreshMyToken(token)\n * },\n * onUnauthorized: () => window.location.href = '/login',\n * onForbidden: () => showError('Access denied')\n * };\n *\n * // Enterprise: Multi-tenant with custom storage\n * const enterpriseConfig: CoSecConfig = {\n * appId: 'my-saas-app',\n * tokenStorage: myCustomTokenStorage,\n * deviceIdStorage: myCustomDeviceStorage,\n * tokenRefresher: myTokenRefresher,\n * spaceIdProvider: mySpaceProvider,\n * onUnauthorized: handle401,\n * onForbidden: handle403\n * };\n * ```\n */\nexport interface CoSecConfig\n extends AppIdCapable, Partial<DeviceIdStorageCapable> {\n /**\n * Your application's unique identifier in the CoSec authentication system.\n *\n * This ID is sent with every request in the `CoSec-App-Id` header and is\n * used to identify which application is making the request. Obtain this\n * value from your CoSec administration console.\n *\n * @remarks\n * **Requirements:**\n * - Must be a non-empty string\n * - Must be registered in the CoSec system\n * - Should be unique per application/deployment\n *\n * **Usage:**\n * ```typescript\n * const config: CoSecConfig = {\n * appId: 'my-production-app-001' // Registered app ID\n * };\n * ```\n */\n readonly appId: string;\n\n /**\n * Custom implementation for storing and retrieving JWT tokens.\n *\n * If not provided, a default TokenStorage instance will be created\n * using browser localStorage. Use this property to provide custom\n * storage backends or testing implementations.\n *\n * @defaultValue new TokenStorage()\n *\n * @example\n * ```typescript\n * // Custom encrypted storage\n * const encryptedStorage = new EncryptedTokenStorage();\n * const config: CoSecConfig = {\n * appId: 'my-app',\n * tokenStorage: encryptedStorage\n * };\n *\n * // Memory storage for testing\n * const memoryStorage = new InMemoryTokenStorage();\n * const config: CoSecConfig = {\n * appId: 'my-app',\n * tokenStorage: memoryStorage\n * };\n * ```\n */\n readonly tokenStorage?: TokenStorage;\n\n /**\n * Custom implementation for storing and retrieving device identifiers.\n *\n * If not provided, a default DeviceIdStorage instance will be created\n * using browser localStorage with cross-tab synchronization. Use this\n * property for custom device identification strategies.\n *\n * @defaultValue new DeviceIdStorage()\n *\n * @example\n * ```typescript\n * // Custom device ID with custom key\n * const customDeviceStorage = new DeviceIdStorage({\n * key: 'my-app-device-id',\n * storage: sessionStorage\n * });\n * const config: CoSecConfig = {\n * appId: 'my-app',\n * deviceIdStorage: customDeviceStorage\n * };\n * ```\n */\n readonly deviceIdStorage?: DeviceIdStorage;\n\n /**\n * Token refresh handler for automatic JWT token renewal.\n *\n * When provided, enables full JWT authentication including:\n * - Automatic Bearer token injection in requests\n * - Token refresh on 401 responses\n * - Persistent token storage\n *\n * When not provided, no authentication interceptors are added.\n *\n * @defaultValue undefined (no authentication)\n *\n * @example\n * ```typescript\n * // Basic refresh implementation\n * const myRefresher: TokenRefresher = {\n * refresh: async (token) => {\n * const response = await fetch('/api/auth/refresh', {\n * method: 'POST',\n * headers: { 'Content-Type': 'application/json' },\n * body: JSON.stringify({ refreshToken: token.refreshToken })\n * });\n * if (!response.ok) throw new Error('Refresh failed');\n * return response.json();\n * }\n * };\n *\n * const config: CoSecConfig = {\n * appId: 'my-app',\n * tokenRefresher: myRefresher\n * };\n * ```\n */\n readonly tokenRefresher?: TokenRefresher;\n\n /**\n * Provider for resolving space identifiers from requests.\n *\n * Used for multi-tenant applications to scope requests to specific\n * spaces within a tenant. When provided, the CoSec-Space-Id header\n * will be added to requests for space-scoped resources.\n *\n * @defaultValue NoneSpaceIdProvider (no space identification)\n *\n * @example\n * ```typescript\n * // Multi-tenant space provider\n * const spaceProvider: SpaceIdProvider = {\n * resolveSpaceId: (exchange) => {\n * return exchange.request.headers['X-Current-Space'] || null;\n * }\n * };\n *\n * const config: CoSecConfig = {\n * appId: 'my-saas-app',\n * tokenRefresher: myRefresher,\n * spaceIdProvider: spaceProvider\n * };\n * ```\n */\n readonly spaceIdProvider?: SpaceIdProvider;\n\n /**\n * Handler invoked when an HTTP 401 Unauthorized response is received.\n *\n * This callback is triggered when the server returns a 401 status code,\n * typically indicating expired or invalid authentication. Use this to\n * implement custom error handling such as redirecting to login.\n *\n * @param exchange - The fetch exchange containing the failed request\n * @returns Promise<void> or void\n *\n * @defaultValue undefined (401 errors not intercepted)\n *\n * @example\n * ```typescript\n * // Redirect to login\n * const config: CoSecConfig = {\n * appId: 'my-app',\n * tokenRefresher: myRefresher,\n * onUnauthorized: async (exchange) => {\n * await authService.logout();\n * window.location.href = '/login?reason=session_expired';\n * }\n * };\n *\n * // Silent token refresh\n * const config: CoSecConfig = {\n * appId: 'my-app',\n * tokenRefresher: myRefresher,\n * onUnauthorized: async (exchange) => {\n * // Force logout without redirect\n * authService.clearSession();\n * dispatch(logoutAction());\n * }\n * };\n * ```\n */\n readonly onUnauthorized?: (exchange: FetchExchange) => Promise<void> | void;\n\n /**\n * Handler invoked when an HTTP 403 Forbidden response is received.\n *\n * This callback is triggered when the server returns a 403 status code,\n * indicating the authenticated user lacks permission for the requested\n * resource. Use this to implement custom access denial handling.\n *\n * @param exchange - The fetch exchange containing the failed request\n * @returns Promise<void>\n *\n * @defaultValue undefined (403 errors not intercepted)\n *\n * @example\n * ```typescript\n * // Show permission error\n * const config: CoSecConfig = {\n * appId: 'my-app',\n * tokenRefresher: myRefresher,\n * onForbidden: async (exchange) => {\n * notification.error({\n * message: 'Access Denied',\n * description: 'You do not have permission to access this resource.'\n * });\n * }\n * };\n *\n * // Redirect to access denied page\n * const config: CoSecConfig = {\n * appId: 'my-app',\n * onForbidden: async (exchange) => {\n * router.push('/access-denied');\n * }\n * };\n * ```\n */\n readonly onForbidden?: (exchange: FetchExchange) => Promise<void>;\n}\n\n/**\n * Configurer class that applies CoSec security interceptors to a Fetcher instance.\n *\n * This class provides a simplified, declarative way to configure all CoSec\n * authentication and security features. It implements FetcherConfigurer and\n * handles the conditional creation and registration of interceptors based\n * on the provided configuration.\n *\n * @remarks\n * **Architecture:**\n * The configurer acts as a composition root for all CoSec interceptors,\n * managing their lifecycle and ensuring proper ordering in the interceptor\n * chain. This approach provides a clean separation between configuration\n * and implementation.\n *\n * **Interceptor Ordering:**\n * Interceptors are registered in a specific order to ensure correct\n * header injection and error handling:\n * 1. CoSecRequestInterceptor (request) - Adds security headers\n * 2. ResourceAttributionRequestInterceptor (request) - Adds tenant attribution\n * 3. AuthorizationRequestInterceptor (request) - Adds Bearer token [conditional]\n * 4. AuthorizationResponseInterceptor (response) - Handles 401 refresh [conditional]\n * 5. UnauthorizedErrorInterceptor (error) - Handles 401 errors [conditional]\n * 6. ForbiddenErrorInterceptor (error) - Handles 403 errors [conditional]\n *\n * **Conditional Features:**\n * - Authentication (tokenRefresher): Enables JWT Bearer token injection and refresh\n * - Space Support (spaceIdProvider): Enables multi-tenant space scoping\n * - Error Handlers (onUnauthorized/onForbidden): Enables custom error handling\n *\n * @example\n * ```typescript\n * import { Fetcher } from '@ahoo-wang/fetcher';\n * import { CoSecConfigurer } from '@ahoo-wang/fetcher-cosec';\n *\n * // Create and configure\n * const configurer = new CoSecConfigurer({\n * appId: 'my-saas-app',\n * tokenRefresher: {\n * refresh: async (token) => {\n * const res = await fetch('/api/refresh', {\n * method: 'POST',\n * body: JSON.stringify({ refreshToken: token.refreshToken })\n * });\n * return res.json();\n * }\n * },\n * onUnauthorized: () => window.location.href = '/login',\n * onForbidden: () => showAccessDenied()\n * });\n *\n * // Apply to fetcher\n * const fetcher = new Fetcher({ baseUrl: 'https://api.example.com' });\n * configurer.applyTo(fetcher);\n *\n * // All requests now have CoSec headers and JWT auth\n * const users = await fetcher.get('/api/users');\n * ```\n *\n * @example\n * ```typescript\n * // Minimal setup - headers only, no authentication\n * const fetcher = new Fetcher();\n * new CoSecConfigurer({\n * appId: 'my-tracking-app'\n * }).applyTo(fetcher);\n *\n * // Requests will include:\n * // - CoSec-App-Id: my-tracking-app\n * // - CoSec-Device-Id: <generated>\n * // - CoSec-Request-Id: <unique-per-request>\n * ```\n */\nexport class CoSecConfigurer implements FetcherConfigurer {\n /**\n * Token storage instance for JWT token persistence.\n *\n * Created from config.tokenStorage or instantiated with default TokenStorage.\n * Used by authorization interceptors to retrieve and store JWT tokens.\n */\n readonly tokenStorage: TokenStorage;\n\n /**\n * Device ID storage instance for device identifier management.\n *\n * Created from config.deviceIdStorage or instantiated with default DeviceIdStorage.\n * Provides persistent device identification with cross-tab synchronization.\n */\n readonly deviceIdStorage: DeviceIdStorage;\n\n /**\n * JWT token manager for authentication operations.\n *\n * Only created when tokenRefresher is provided in the config.\n * When undefined, no authentication interceptors are registered.\n */\n readonly tokenManager?: JwtTokenManager;\n\n /**\n * Space identifier provider for multi-tenant support.\n *\n * Created from config.spaceIdProvider or defaulted to NoneSpaceIdProvider.\n * Resolves space IDs from requests for tenant-scoped resource access.\n */\n readonly spaceIdProvider?: SpaceIdProvider;\n\n /**\n * Creates a new CoSecConfigurer instance with the provided configuration.\n *\n * The constructor performs dependency initialization:\n * - Creates or wraps tokenStorage and deviceIdStorage with defaults\n * - Initializes spaceIdProvider (defaults to NoneSpaceIdProvider)\n * - Conditionally creates tokenManager only if tokenRefresher is provided\n *\n * @param config - CoSec configuration object containing all settings\n *\n * @throws Error if appId is not provided\n * @throws Error if provided dependencies are invalid\n *\n * @example\n * ```typescript\n * // Full configuration\n * const configurer = new CoSecConfigurer({\n * appId: 'my-app',\n * tokenStorage: customTokenStorage,\n * deviceIdStorage: customDeviceStorage,\n * tokenRefresher: myRefresher,\n * spaceIdProvider: mySpaceProvider,\n * onUnauthorized: handle401,\n * onForbidden: handle403\n * });\n *\n * // Minimal configuration\n * const configurer = new CoSecConfigurer({\n * appId: 'my-app'\n * });\n *\n * // Custom storage with default refresh\n * const configurer = new CoSecConfigurer({\n * appId: 'my-app',\n * tokenStorage: encryptedStorage,\n * deviceIdStorage: customDeviceStorage\n * });\n * ```\n */\n constructor(public readonly config: CoSecConfig) {\n // Initialize storage instances with provided or default implementations\n this.tokenStorage = config.tokenStorage ?? new TokenStorage();\n this.deviceIdStorage = config.deviceIdStorage ?? new DeviceIdStorage();\n this.spaceIdProvider = config.spaceIdProvider ?? NoneSpaceIdProvider;\n\n // Conditionally create token manager for authentication\n if (config.tokenRefresher) {\n this.tokenManager = new JwtTokenManager(\n this.tokenStorage,\n config.tokenRefresher,\n );\n }\n }\n\n /**\n * Applies CoSec interceptors to the specified Fetcher instance.\n *\n * This method registers all configured interceptors with the Fetcher's\n * interceptor chain. The registration is conditional based on the config:\n *\n * **Always Registered (Headers & Attribution):**\n * - CoSecRequestInterceptor: Injects security headers\n * - ResourceAttributionRequestInterceptor: Adds tenant attribution parameters\n *\n * **Conditional Registration (Authentication):**\n * - AuthorizationRequestInterceptor: Adds Bearer token [requires tokenRefresher]\n * - AuthorizationResponseInterceptor: Handles 401 refresh [requires tokenRefresher]\n *\n * **Conditional Registration (Error Handling):**\n * - UnauthorizedErrorInterceptor: Custom 401 handling [requires onUnauthorized]\n * - ForbiddenErrorInterceptor: Custom 403 handling [requires onForbidden]\n *\n * @param fetcher - The Fetcher instance to configure with CoSec interceptors\n *\n * @throws Error if fetcher is null or undefined\n *\n * @example\n * ```typescript\n * const fetcher = new Fetcher({ baseUrl: 'https://api.example.com' });\n *\n * const configurer = new CoSecConfigurer({\n * appId: 'my-app',\n * tokenRefresher: myRefresher,\n * onUnauthorized: () => redirectToLogin(),\n * onForbidden: () => showError()\n * });\n *\n * configurer.applyTo(fetcher);\n *\n * // Now fetcher has all CoSec interceptors configured\n * const data = await fetcher.get('/api/data');\n * ```\n *\n * @example\n * ```typescript\n * // Applying to multiple fetchers\n * const apiFetcher = new Fetcher({ baseUrl: 'https://api.example.com' });\n * const adminFetcher = new Fetcher({ baseUrl: 'https://admin.example.com' });\n *\n * const configurer = new CoSecConfigurer({\n * appId: 'my-app',\n * tokenRefresher: myRefresher,\n * onForbidden: showAccessDenied\n * });\n *\n * // Apply same configuration to multiple fetchers\n * configurer.applyTo(apiFetcher);\n * configurer.applyTo(adminFetcher);\n * ```\n */\n applyTo(fetcher: Fetcher): void {\n // Register CoSec request interceptor for security headers\n fetcher.interceptors.request.use(\n new CoSecRequestInterceptor({\n appId: this.config.appId,\n deviceIdStorage: this.deviceIdStorage,\n spaceIdProvider: this.spaceIdProvider,\n }),\n );\n\n // Register resource attribution for tenant/owner path parameters\n fetcher.interceptors.request.use(\n new ResourceAttributionRequestInterceptor({\n tokenStorage: this.tokenStorage,\n }),\n );\n\n // Conditionally register authentication interceptors\n if (this.tokenManager) {\n // Authorization header injection\n fetcher.interceptors.request.use(\n new AuthorizationRequestInterceptor({\n tokenManager: this.tokenManager,\n }),\n );\n\n // 401 response handling and token refresh\n fetcher.interceptors.response.use(\n new AuthorizationResponseInterceptor({\n tokenManager: this.tokenManager,\n }),\n );\n }\n\n // Conditionally register error handlers\n if (this.config.onUnauthorized) {\n fetcher.interceptors.error.use(\n new UnauthorizedErrorInterceptor({\n onUnauthorized: this.config.onUnauthorized,\n }),\n );\n }\n\n if (this.config.onForbidden) {\n fetcher.interceptors.error.use(\n new ForbiddenErrorInterceptor({\n onForbidden: this.config.onForbidden,\n }),\n );\n }\n }\n}\n","/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { Fetcher, ResultExtractors } from '@ahoo-wang/fetcher';\nimport { IGNORE_REFRESH_TOKEN_ATTRIBUTE_KEY } from './cosecRequestInterceptor';\n\n/**\n * Interface representing an access token used for authentication.\n *\n * This interface defines the structure for access tokens, which are typically\n * short-lived tokens used to authenticate API requests.\n *\n * @interface AccessToken\n * @property {string} accessToken - The access token string used for authentication.\n */\nexport interface AccessToken {\n accessToken: string;\n}\n\n/**\n * Interface representing a refresh token used to obtain new access tokens.\n *\n * Refresh tokens are long-lived tokens that can be used to request new access\n * tokens without requiring the user to re-authenticate.\n *\n * @interface RefreshToken\n * @property {string} refreshToken - The refresh token string used to obtain new access tokens.\n */\nexport interface RefreshToken {\n refreshToken: string;\n}\n\n/**\n * Composite token interface that contains both access and refresh tokens.\n *\n * This interface combines AccessToken and RefreshToken, ensuring that both\n * tokens are always handled together. In authentication systems, access and\n * refresh tokens are typically issued and refreshed as a pair.\n *\n * @interface CompositeToken\n * @extends AccessToken\n * @extends RefreshToken\n * @property {string} accessToken - The access token string used for authentication.\n * @property {string} refreshToken - The refresh token string used to obtain new access tokens.\n */\nexport interface CompositeToken extends AccessToken, RefreshToken {}\n\n/**\n * Interface for token refreshers.\n *\n * This interface defines the contract for classes responsible for refreshing\n * authentication tokens. Implementations should handle the logic of exchanging\n * an expired or soon-to-expire token for a new one.\n *\n * **CRITICAL**: When implementing the `refresh` method, always include the\n * `attributes: new Map([[IGNORE_REFRESH_TOKEN_ATTRIBUTE_KEY, true]])` in the\n * request options to prevent infinite loops. Without this attribute, the request\n * interceptor may attempt to refresh the token recursively, causing a deadlock.\n *\n * @interface TokenRefresher\n */\nexport interface TokenRefresher {\n /**\n * Refreshes the given composite token and returns a new one.\n *\n * This method should send the provided token to the appropriate endpoint\n * and return the refreshed token pair. **IMPORTANT**: The implementation must\n * include `attributes: new Map([[IGNORE_REFRESH_TOKEN_ATTRIBUTE_KEY, true]])`\n * in the request options to prevent recursive refresh attempts that could\n * cause infinite loops.\n *\n * @param {CompositeToken} token - The composite token to refresh, containing both access and refresh tokens.\n * @returns {Promise<CompositeToken>} A promise that resolves to a new CompositeToken with updated access and refresh tokens.\n * @throws {Error} Throws an error if the token refresh fails due to network issues, invalid tokens, or server errors.\n *\n * @example\n * ```typescript\n * const refresher: TokenRefresher = new CoSecTokenRefresher({ fetcher, endpoint: '/auth/refresh' });\n * const newToken = await refresher.refresh(currentToken);\n * console.log('New access token:', newToken.accessToken);\n * ```\n *\n * @warning **Critical**: Failing to set the `IGNORE_REFRESH_TOKEN_ATTRIBUTE_KEY`\n * attribute will result in infinite loops during token refresh operations.\n */\n refresh(token: CompositeToken): Promise<CompositeToken>;\n}\n\n/**\n * Configuration options for the CoSecTokenRefresher class.\n *\n * This interface defines the required options to initialize a CoSecTokenRefresher\n * instance, including the HTTP client and the refresh endpoint URL.\n *\n * @interface CoSecTokenRefresherOptions\n * @property {Fetcher} fetcher - The HTTP client instance used to make requests to the refresh endpoint.\n * @property {string} endpoint - The URL endpoint where token refresh requests are sent.\n */\nexport interface CoSecTokenRefresherOptions {\n fetcher: Fetcher;\n endpoint: string;\n}\n\n/**\n * Implementation of TokenRefresher for refreshing composite tokens using CoSec authentication.\n *\n * This class provides a concrete implementation of the TokenRefresher interface,\n * using the provided Fetcher instance to send POST requests to a specified endpoint\n * for token refresh operations. It is designed to work with CoSec (Corporate Security)\n * authentication systems.\n *\n * @class CoSecTokenRefresher\n * @implements TokenRefresher\n *\n * @example\n * ```typescript\n * import { Fetcher } from '@ahoo-wang/fetcher';\n * import { CoSecTokenRefresher } from './tokenRefresher';\n *\n * const fetcher = new Fetcher();\n * const refresher = new CoSecTokenRefresher({\n * fetcher,\n * endpoint: 'https://api.example.com/auth/refresh'\n * });\n *\n * const currentToken = { accessToken: 'old-token', refreshToken: 'refresh-token' };\n * const newToken = await refresher.refresh(currentToken);\n * ```\n */\nexport class CoSecTokenRefresher implements TokenRefresher {\n /**\n * Creates a new instance of CoSecTokenRefresher.\n *\n * @param {CoSecTokenRefresherOptions} options - The configuration options for the token refresher.\n * @param {Fetcher} options.fetcher - The HTTP client instance used for making refresh requests.\n * @param {string} options.endpoint - The URL endpoint for token refresh operations.\n *\n * @example\n * ```typescript\n * const refresher = new CoSecTokenRefresher({\n * fetcher: myFetcherInstance,\n * endpoint: '/api/v1/auth/refresh'\n * });\n * ```\n */\n constructor(public readonly options: CoSecTokenRefresherOptions) {}\n\n /**\n * Refreshes the given composite token by sending a POST request to the configured endpoint.\n *\n * This method sends the current token pair to the refresh endpoint and expects\n * a new CompositeToken in response.\n *\n * **CRITICAL**: The request includes a special\n * attribute `attributes: new Map([[IGNORE_REFRESH_TOKEN_ATTRIBUTE_KEY, true]])`\n * to prevent infinite loops. Without this attribute, the request interceptor\n * may attempt to refresh the token again, causing a recursive loop.\n *\n * @param {CompositeToken} token - The composite token to refresh, containing both access and refresh tokens.\n * @returns {Promise<CompositeToken>} A promise that resolves to a new CompositeToken with refreshed tokens.\n * @throws {Error} Throws an error if the HTTP request fails, the server returns an error status, or the response cannot be parsed as JSON.\n * @throws {NetworkError} Throws a network error if there are connectivity issues.\n * @throws {AuthenticationError} Throws an authentication error if the refresh token is invalid or expired.\n *\n * @example\n * ```typescript\n * const refresher = new CoSecTokenRefresher({ fetcher, endpoint: '/auth/refresh' });\n * const token = { accessToken: 'expired-token', refreshToken: 'valid-refresh-token' };\n *\n * try {\n * const newToken = await refresher.refresh(token);\n * console.log('Refreshed access token:', newToken.accessToken);\n * } catch (error) {\n * console.error('Token refresh failed:', error.message);\n * }\n * ```\n *\n * @warning **Important**: Always include the `IGNORE_REFRESH_TOKEN_ATTRIBUTE_KEY` attribute\n * in refresh requests to avoid infinite loops caused by recursive token refresh attempts.\n */\n refresh(token: CompositeToken): Promise<CompositeToken> {\n // Send a POST request to the configured endpoint with the token as body\n // and extract the response as JSON to return a new CompositeToken\n\n return this.options.fetcher.post<CompositeToken>(\n this.options.endpoint,\n {\n body: token,\n },\n {\n resultExtractor: ResultExtractors.Json,\n attributes: new Map([[IGNORE_REFRESH_TOKEN_ATTRIBUTE_KEY, true]]),\n },\n );\n }\n}\n"],"names":["_CoSecHeaders","CoSecHeaders","_ResponseCodes","ResponseCodes","AuthorizeResults","NanoIdGenerator","nanoid","idGenerator","NoneSpaceIdProvider","DEFAULT_COSEC_SPACE_ID_KEY","SpaceIdStorage","KeyStorage","key","eventBus","BroadcastTypedEventBus","SerialTypedEventBus","reset","typedIdentitySerializer","DefaultSpaceIdProvider","options","exchange","COSEC_REQUEST_INTERCEPTOR_NAME","COSEC_REQUEST_INTERCEPTOR_ORDER","DEFAULT_INTERCEPTOR_ORDER_STEP","IGNORE_REFRESH_TOKEN_ATTRIBUTE_KEY","CoSecRequestInterceptor","appId","deviceIdStorage","spaceIdProvider","requestId","deviceId","spaceId","requestHeaders","AUTHORIZATION_REQUEST_INTERCEPTOR_NAME","AUTHORIZATION_REQUEST_INTERCEPTOR_ORDER","AuthorizationRequestInterceptor","currentToken","AUTHORIZATION_RESPONSE_INTERCEPTOR_NAME","AUTHORIZATION_RESPONSE_INTERCEPTOR_ORDER","AuthorizationResponseInterceptor","response","error","DEFAULT_COSEC_DEVICE_ID_KEY","DeviceIdStorage","FORBIDDEN_ERROR_INTERCEPTOR_NAME","FORBIDDEN_ERROR_INTERCEPTOR_ORDER","ForbiddenErrorInterceptor","RefreshTokenError","FetcherError","token","cause","JwtTokenManager","tokenStorage","tokenRefresher","jwtToken","newToken","TENANT_ID_PATH_KEY","OWNER_ID_PATH_KEY","RESOURCE_ATTRIBUTION_REQUEST_INTERCEPTOR_NAME","RESOURCE_ATTRIBUTION_REQUEST_INTERCEPTOR_ORDER","URL_RESOLVE_INTERCEPTOR_ORDER","ResourceAttributionRequestInterceptor","tenantId","ownerId","principal","extractedPathParams","tenantIdPathKey","requestPathParams","ownerIdPathKey","parseJwtPayload","parts","base64","paddedBase64","jsonPayload","isTokenExpired","earlyPeriod","payload","expAt","JwtToken","JwtCompositeToken","JwtCompositeTokenSerializer","value","compositeToken","jwtCompositeTokenSerializer","DEFAULT_COSEC_TOKEN_KEY","TokenStorage","UNAUTHORIZED_ERROR_INTERCEPTOR_NAME","UNAUTHORIZED_ERROR_INTERCEPTOR_ORDER","UnauthorizedErrorInterceptor","CoSecConfigurer","config","fetcher","CoSecTokenRefresher","ResultExtractors"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.es.js","names":[],"sources":["../src/types.ts","../src/idGenerator.ts","../src/spaceIdProvider.ts","../src/cosecRequestInterceptor.ts","../src/authorizationRequestInterceptor.ts","../src/authorizationResponseInterceptor.ts","../src/deviceIdStorage.ts","../src/forbiddenErrorInterceptor.ts","../src/jwtTokenManager.ts","../src/resourceAttributionRequestInterceptor.ts","../src/jwts.ts","../src/jwtToken.ts","../src/tokenStorage.ts","../src/unauthorizedErrorInterceptor.ts","../src/cosecConfigurer.ts","../src/tokenRefresher.ts"],"sourcesContent":["/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { DeviceIdStorage } from './deviceIdStorage';\nimport type { JwtTokenManager } from './jwtTokenManager';\n\n/**\n * CoSec HTTP headers enumeration.\n */\nexport class CoSecHeaders {\n static readonly DEVICE_ID = 'CoSec-Device-Id';\n static readonly APP_ID = 'CoSec-App-Id';\n static readonly SPACE_ID = 'CoSec-Space-Id';\n static readonly AUTHORIZATION = 'Authorization';\n static readonly REQUEST_ID = 'CoSec-Request-Id';\n}\n\nexport class ResponseCodes {\n static readonly UNAUTHORIZED = 401;\n static readonly FORBIDDEN = 403;\n}\n\nexport interface AppIdCapable {\n /**\n * Application ID to be sent in the CoSec-App-Id header.\n */\n appId: string;\n}\n\nexport interface DeviceIdStorageCapable {\n deviceIdStorage: DeviceIdStorage;\n}\n\nexport interface JwtTokenManagerCapable {\n tokenManager: JwtTokenManager;\n}\n\n/**\n * CoSec options interface.\n */\nexport interface CoSecOptions\n extends AppIdCapable,\n DeviceIdStorageCapable,\n JwtTokenManagerCapable {\n}\n\n/**\n * Authorization result interface.\n */\nexport interface AuthorizeResult {\n authorized: boolean;\n reason: string;\n}\n\n/**\n * Authorization result constants.\n */\nexport const AuthorizeResults = {\n ALLOW: { authorized: true, reason: 'Allow' },\n EXPLICIT_DENY: { authorized: false, reason: 'Explicit Deny' },\n IMPLICIT_DENY: { authorized: false, reason: 'Implicit Deny' },\n TOKEN_EXPIRED: { authorized: false, reason: 'Token Expired' },\n TOO_MANY_REQUESTS: { authorized: false, reason: 'Too Many Requests' },\n};\n","/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { nanoid } from 'nanoid';\n\nexport interface IdGenerator {\n generateId(): string;\n}\n\n/**\n * Nano ID implementation of IdGenerator.\n * Generates unique request IDs using Nano ID.\n */\nexport class NanoIdGenerator implements IdGenerator {\n /**\n * Generate a unique request ID.\n *\n * @returns A unique request ID\n */\n generateId(): string {\n return nanoid();\n }\n}\n\nexport const idGenerator = new NanoIdGenerator();\n","/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type {\n KeyStorageOptions} from '@ahoo-wang/fetcher-storage';\nimport {\n KeyStorage,\n typedIdentitySerializer,\n} from '@ahoo-wang/fetcher-storage';\nimport {\n BroadcastTypedEventBus,\n SerialTypedEventBus,\n} from '@ahoo-wang/fetcher-eventbus';\nimport type { FetchExchange } from '@ahoo-wang/fetcher';\n\n/**\n * Represents a space identifier for resource scoping within a tenant.\n *\n * A SpaceId uniquely identifies a space (workspace, project, organization unit, etc.)\n * that belongs to a tenant. This enables multi-tenant applications to further\n * categorize resources into discrete spaces for access control and data isolation.\n *\n * In a typical multi-tenant architecture:\n * - TenantId: Identifies the tenant/organization\n * - SpaceId: Identifies a space within that tenant (e.g., team, project, department)\n *\n * @example\n * ```\n * // A tenant might have multiple spaces:\n * Tenant: \"company-abc\"\n * Space: \"engineering-team\"\n * Space: \"marketing-team\"\n * Space: \"project-alpha\"\n * ```\n */\ntype SpaceId = string;\n\n/**\n * Interface for resolving space identifiers from HTTP request exchanges.\n *\n * A SpaceIdProvider determines which space a particular request belongs to\n * based on the incoming request characteristics. This abstraction allows\n * different strategies for space identification, such as header-based,\n * URL-based, or cookie-based resolution.\n *\n * @remarks\n * Implementations of this interface are used by the CoSecRequestInterceptor\n * to add the appropriate space identifier to outgoing requests. This enables\n * proper routing and access control in multi-tenant, multi-space environments.\n *\n * @example\n * ```typescript\n * // Example: Extract space ID from a custom header\n * const headerSpaceIdProvider: SpaceIdProvider = {\n * resolveSpaceId: (exchange) => {\n * return exchange.request.headers['X-Space-Id'] || null;\n * }\n * };\n * ```\n */\nexport interface SpaceIdProvider {\n /**\n * Resolves the space identifier for a given fetch exchange.\n *\n * This method examines the request exchange and determines which space\n * the request should be associated with. The resolution strategy depends\n * on the implementation - it may examine headers, URL patterns, cookies,\n * or other request attributes.\n *\n * @param exchange - The fetch exchange containing the HTTP request details\n * @returns The resolved space identifier, or null if no space can be determined\n *\n * @example\n * ```typescript\n * // Example: Extract space ID from a custom header\n * const provider: SpaceIdProvider = {\n * resolveSpaceId: (exchange) => {\n * return exchange.request.headers['X-Space-Id'] || null;\n * }\n * };\n *\n * // Example: Extract space ID from URL path\n * const urlSpaceIdProvider: SpaceIdProvider = {\n * resolveSpaceId: (exchange) => {\n * const match = exchange.request.url.match(/\\/spaces\\/([^\\/]+)/);\n * return match ? match[1] : null;\n * }\n * };\n * ```\n */\n resolveSpaceId(exchange: FetchExchange): SpaceId | null;\n}\n\n/**\n * A no-op SpaceIdProvider that always returns null.\n *\n * This provider is used when space identification is not required or when\n * the application operates without space-level resource isolation.\n * It provides a convenient default that requires no configuration.\n *\n * @remarks\n * Use NoneSpaceIdProvider when:\n * - Resources are not organized into spaces\n * - Space context is managed externally\n * - Single-space per tenant deployment\n *\n * @example\n * ```typescript\n * // Single-space configuration (no space-level isolation needed)\n * const interceptor = new CoSecRequestInterceptor({\n * appId: 'my-app',\n * deviceIdStorage,\n * spaceIdProvider: NoneSpaceIdProvider,\n * });\n * ```\n */\nexport const NoneSpaceIdProvider: SpaceIdProvider = {\n resolveSpaceId: () => null,\n};\n\n/**\n * The default storage key used for persisting space identifiers.\n *\n * This key is used by SpaceIdStorage to store and retrieve space identifiers\n * from the browser's localStorage. The default value ensures consistency\n * across different parts of the application that need to access the stored space ID.\n */\nexport const DEFAULT_COSEC_SPACE_ID_KEY = 'cosec-space-id';\n\n/**\n * Configuration options for SpaceIdStorage.\n *\n * This interface extends KeyStorageOptions to provide SpaceId-specific\n * configuration while allowing customization of the underlying storage behavior.\n * All properties from KeyStorageOptions are optional and will use sensible defaults.\n *\n * @remarks\n * When no options are provided, SpaceIdStorage will use:\n * - DEFAULT_COSEC_SPACE_ID_KEY as the storage key\n * - A BroadcastTypedEventBus for cross-tab synchronization\n * - The browser's localStorage for persistence\n * - The identity serializer for direct string storage\n *\n * @example\n * ```typescript\n * // Default configuration\n * const storage = new SpaceIdStorage();\n *\n * // Custom key\n * const customStorage = new SpaceIdStorage({\n * key: 'my-custom-space-key'\n * });\n *\n * // Custom storage with event bus\n * const storageWithCustomBus = new SpaceIdStorage({\n * key: 'space-id',\n * eventBus: myCustomEventBus,\n * storage: myCustomStorage\n * });\n * ```\n */\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface SpaceIdStorageOptions extends Partial<\n KeyStorageOptions<SpaceId>\n> {}\n\n/**\n * Storage class for managing space identifiers with persistence and cross-tab synchronization.\n *\n * SpaceIdStorage extends KeyStorage to provide specialized storage for space\n * identifiers. It handles persistence to localStorage and provides automatic\n * synchronization across multiple browser tabs using the BroadcastChannel API.\n *\n * @remarks\n * Key features:\n * - Automatic persistence to localStorage\n * - Cross-tab synchronization via BroadcastChannel\n * - Caching for performance optimization\n * - Event-based notification of changes\n *\n * The storage uses the identity serializer, meaning space IDs are stored\n * and retrieved as-is without any transformation.\n *\n * @example\n * ```typescript\n * // Basic usage\n * const spaceStorage = new SpaceIdStorage();\n * spaceStorage.set('workspace-alpha');\n * const spaceId = spaceStorage.get(); // 'workspace-alpha'\n *\n * // Listening for changes\n * const removeListener = spaceStorage.addListener({\n * name: 'space-change',\n * handle: (event) => {\n * console.log('Space changed from', event.oldValue, 'to', event.newValue);\n * }\n * });\n *\n * // Cleanup\n * spaceStorage.destroy();\n * removeListener();\n * ```\n */\nexport class SpaceIdStorage extends KeyStorage<SpaceId> {\n /**\n * Creates a new SpaceIdStorage instance.\n *\n * @param options - Optional configuration options for the storage\n * @param options.key - The storage key (defaults to DEFAULT_COSEC_SPACE_ID_KEY)\n * @param options.eventBus - Custom event bus for cross-tab communication\n * @param options.storage - Custom storage implementation (defaults to localStorage)\n * @param options.serializer - Custom serializer (defaults to identity serializer)\n * @param options.defaultValue - Default value when no space ID is stored\n *\n * @throws Error if the underlying storage is unavailable\n *\n * @example\n * ```typescript\n * // Default configuration\n * const storage = new SpaceIdStorage();\n *\n * // With custom options\n * const storage = new SpaceIdStorage({\n * key: 'custom-space-key',\n * storage: sessionStorage,\n * defaultValue: 'default-space'\n * });\n * ```\n */\n constructor({\n key = DEFAULT_COSEC_SPACE_ID_KEY,\n eventBus = new BroadcastTypedEventBus({\n delegate: new SerialTypedEventBus(DEFAULT_COSEC_SPACE_ID_KEY),\n }),\n ...reset\n }: SpaceIdStorageOptions = {}) {\n super({ key, eventBus, ...reset, serializer: typedIdentitySerializer() });\n }\n}\n\n/**\n * Interface for predicates that determine if a resource requires space scoping.\n *\n * A SpacedResourcePredicate is used to evaluate whether a given request exchange\n * should be associated with a space identifier. This allows fine-grained control\n * over which resources belong to spaces and require space-based access control.\n *\n * @remarks\n * Predicates can implement various strategies:\n * - URL pattern matching (e.g., /spaces/{spaceId}/resources)\n * - Header presence checks\n * - Method-based filtering\n * - Custom business logic\n *\n * @example\n * ```typescript\n * // URL-based predicate for space-scoped resources\n * const urlPredicate: SpacedResourcePredicate = {\n * test: (exchange) => {\n * return exchange.request.url.includes('/spaces/');\n * }\n * };\n *\n * // Header-based predicate\n * const headerPredicate: SpacedResourcePredicate = {\n * test: (exchange) => {\n * return 'X-Require-Space' in exchange.request.headers;\n * }\n * };\n *\n * // Composite predicate using logical operations\n * const compositePredicate: SpacedResourcePredicate = {\n * test: (exchange) => {\n * return urlPredicate.test(exchange) && headerPredicate.test(exchange);\n * }\n * };\n * ```\n */\nexport interface SpacedResourcePredicate {\n /**\n * Tests whether the given exchange represents a space-scoped resource.\n *\n * This method evaluates the request exchange against the predicate's criteria\n * to determine if space-based routing or access control should be applied.\n *\n * @param exchange - The fetch exchange to evaluate\n * @returns true if the resource requires space scoping, false otherwise\n *\n * @example\n * ```typescript\n * const predicate: SpacedResourcePredicate = {\n * test: (exchange) => {\n * // Only resources under /api/spaces/ require space identification\n * return exchange.request.url.startsWith('/api/v1/spaces/');\n * }\n * };\n * ```\n */\n test(exchange: FetchExchange): boolean;\n}\n\n/**\n * Configuration options for DefaultSpaceIdProvider.\n *\n * This interface combines the predicate and storage dependencies required\n * by DefaultSpaceIdProvider to resolve space identifiers.\n */\nexport interface SpaceIdProviderOptions {\n /**\n * The predicate used to determine if a request requires space scoping.\n *\n * This predicate is consulted before attempting to retrieve the space ID\n * from storage. If the predicate returns false, no space ID will be returned.\n */\n spacedResourcePredicate: SpacedResourcePredicate;\n\n /**\n * The storage instance for persisting and retrieving space identifiers.\n *\n * This storage is used to look up the space ID once the predicate\n * confirms that the request requires space scoping.\n */\n spaceIdStorage: SpaceIdStorage;\n}\n\n/**\n * Default implementation of SpaceIdProvider that combines predicate-based\n * filtering with persistent storage.\n *\n * DefaultSpaceIdProvider implements a two-step resolution strategy:\n * 1. First, the predicate determines if the request requires space scoping\n * 2. If yes, the space ID is retrieved from the configured storage\n *\n * This separation enables:\n * - Efficient filtering of non-spaced resources\n * - Flexible predicate logic without storage coupling\n * - Testable and replaceable components\n *\n * @example\n * ```typescript\n * // Create storage for space IDs\n * const spaceStorage = new SpaceIdStorage();\n *\n * // Create predicate for space-scoped resources\n * const spacedResourcePredicate: SpacedResourcePredicate = {\n * test: (exchange) => {\n * return exchange.request.url.includes('/spaces/');\n * }\n * };\n *\n * // Create the provider\n * const spaceIdProvider = new DefaultSpaceIdProvider({\n * spacedResourcePredicate,\n * spaceIdStorage: spaceStorage\n * });\n *\n * // Use with CoSecRequestInterceptor\n * const interceptor = new CoSecRequestInterceptor({\n * appId: 'my-app',\n * deviceIdStorage,\n * spaceIdProvider\n * });\n * ```\n */\nexport class DefaultSpaceIdProvider implements SpaceIdProvider {\n /**\n * Creates a new DefaultSpaceIdProvider instance.\n *\n * @param options - The configuration options containing the predicate and storage\n * @param options.spacedResourcePredicate - Determines which requests require space scoping\n * @param options.spaceIdStorage - Provides access to the persisted space identifier\n *\n * @example\n * ```typescript\n * const provider = new DefaultSpaceIdProvider({\n * spacedResourcePredicate: {\n * test: (exchange) => exchange.request.url.includes('/api/')\n * },\n * spaceIdStorage: new SpaceIdStorage()\n * });\n * ```\n */\n constructor(private options: SpaceIdProviderOptions) {}\n\n /**\n * Resolves the space identifier for a given fetch exchange.\n *\n * This method first checks if the request requires space scoping by\n * evaluating the predicate. If the predicate returns true, it retrieves\n * the space ID from storage. Otherwise, it returns null.\n *\n * @param exchange - The fetch exchange containing the HTTP request details\n * @returns The space identifier if the request requires scoping and one exists,\n * otherwise null\n *\n * @example\n * ```typescript\n * const provider = new DefaultSpaceIdProvider({\n * spacedResourcePredicate: {\n * test: (exchange) => exchange.request.url.includes('/spaced/')\n * },\n * spaceIdStorage: new SpaceIdStorage()\n * });\n *\n * // For a request to /api/spaces/workspace-alpha/resources\n * const exchange = createMockExchange({ url: '/api/spaces/workspace-alpha/resources' });\n * const spaceId = provider.resolveSpaceId(exchange); // Returns stored space ID\n *\n * // For a request to /api/public/resources\n * const publicExchange = createMockExchange({ url: '/api/public/resources' });\n * const noSpace = provider.resolveSpaceId(publicExchange); // Returns null\n * ```\n */\n resolveSpaceId(exchange: FetchExchange): SpaceId | null {\n if (!this.options.spacedResourcePredicate.test(exchange)) {\n return null;\n }\n return this.options.spaceIdStorage.get();\n }\n}\n","/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type {\n FetchExchange} from '@ahoo-wang/fetcher';\nimport {\n DEFAULT_INTERCEPTOR_ORDER_STEP,\n type RequestInterceptor,\n} from '@ahoo-wang/fetcher';\nimport type { AppIdCapable, DeviceIdStorageCapable } from './types';\nimport { CoSecHeaders } from './types';\nimport { idGenerator } from './idGenerator';\nimport type { SpaceIdProvider } from './spaceIdProvider';\nimport { NoneSpaceIdProvider } from './spaceIdProvider';\nimport type { DeviceIdStorage } from './deviceIdStorage';\n\n/**\n * Configuration options for CoSecRequestInterceptor.\n *\n * This interface combines the required dependencies (appId and deviceIdStorage)\n * with an optional spaceIdProvider for multi-tenant, multi-space applications.\n *\n * @remarks\n * The appId identifies your application in the CoSec authentication system.\n * The deviceIdStorage manages persistent device identifiers for tracking.\n * The spaceIdProvider (optional) enables space-scoped resource access control.\n *\n * @example\n * ```typescript\n * // Basic configuration\n * const options: CoSecRequestOptions = {\n * appId: 'my-web-app',\n * deviceIdStorage: new DeviceIdStorage()\n * };\n *\n * // With space support for multi-tenant applications\n * const optionsWithSpace: CoSecRequestOptions = {\n * appId: 'my-web-app',\n * deviceIdStorage: new DeviceIdStorage(),\n * spaceIdProvider: new DefaultSpaceIdProvider({\n * spacedResourcePredicate: urlPredicate,\n * spaceIdStorage: new SpaceIdStorage()\n * })\n * };\n * ```\n */\nexport interface CoSecRequestOptions\n extends AppIdCapable, DeviceIdStorageCapable {\n /**\n * Optional provider for resolving space identifiers from requests.\n *\n * When provided, enables space-scoped resource access by adding the\n * CoSec-Space-Id header to requests for space-scoped resources.\n *\n * @defaultValue NoneSpaceIdProvider (no space identification)\n */\n spaceIdProvider?: SpaceIdProvider;\n}\n\n/**\n * The unique identifier name for the CoSecRequestInterceptor.\n *\n * This constant is used by the Fetcher interceptor registration system\n * to identify and manage this interceptor instance.\n *\n * @example\n * ```typescript\n * // Used internally by Fetcher for interceptor registration\n * fetcher.addInterceptor(new CoSecRequestInterceptor(options));\n * console.log(interceptor.name); // 'CoSecRequestInterceptor'\n * ```\n */\nexport const COSEC_REQUEST_INTERCEPTOR_NAME = 'CoSecRequestInterceptor';\n\n/**\n * The execution order for the CoSecRequestInterceptor.\n *\n * This value is calculated to ensure the interceptor runs after request body\n * processing but before the actual HTTP request is made. The order is set\n * to Number.MIN_SAFE_INTEGER + DEFAULT_INTERCEPTOR_ORDER_STEP to guarantee\n * early execution while maintaining safe integer boundaries.\n *\n * @remarks\n * - Position: After RequestBodyInterceptor\n * - Position: Before FetchInterceptor\n * - Value: Number.MIN_SAFE_INTEGER + 1000\n *\n * @example\n * ```typescript\n * const interceptor = new CoSecRequestInterceptor(options);\n * console.log(interceptor.order); // -9007199254740990\n * ```\n */\nexport const COSEC_REQUEST_INTERCEPTOR_ORDER =\n Number.MIN_SAFE_INTEGER + DEFAULT_INTERCEPTOR_ORDER_STEP;\n\n/**\n * Attribute key used to mark requests that should skip token refresh.\n *\n * When this attribute is set to true on a request exchange, the\n * AuthorizationRequestInterceptor will skip the automatic token refresh\n * for that specific request. This is useful for operations where\n * token refresh would cause issues, such as logout requests.\n *\n * @remarks\n * Set this attribute on the exchange before the AuthorizationRequestInterceptor runs:\n * ```typescript\n * exchange.attributes.set(IGNORE_REFRESH_TOKEN_ATTRIBUTE_KEY, true);\n * ```\n *\n * @example\n * ```typescript\n * // Skip refresh during logout\n * const logoutExchange = await fetcher.createRequest('/logout', {\n * method: 'POST'\n * }).getExchange();\n * logoutExchange.attributes.set(IGNORE_REFRESH_TOKEN_ATTRIBUTE_KEY, true);\n * await fetcher.fetch(logoutExchange);\n * ```\n */\nexport const IGNORE_REFRESH_TOKEN_ATTRIBUTE_KEY = 'Ignore-Refresh-Token';\n\n/**\n * Request interceptor that automatically adds CoSec authentication headers to outgoing requests.\n *\n * CoSecRequestInterceptor enriches each HTTP request with security-related headers\n * that enable the CoSec authentication system to:\n * - Track device identifiers for security monitoring\n * - Identify the application making the request\n * - Correlate requests for logging and analytics\n * - Scope requests to specific spaces in multi-tenant environments\n *\n * @remarks\n * **Header Injection:**\n * This interceptor injects the following headers into every outgoing request:\n * - `CoSec-App-Id`: Identifies your application\n * - `CoSec-Device-Id`: Unique device identifier (persisted or generated)\n * - `CoSec-Request-Id`: Unique identifier for this request (per-request)\n * - `CoSec-Space-Id`: Space identifier (when spaceIdProvider returns a value)\n *\n * **Execution Order:**\n * The interceptor runs at COSEC_REQUEST_INTERCEPTOR_ORDER position, which ensures:\n * 1. Request body processing is complete\n * 2. Authentication headers are not overwritten by subsequent interceptors\n * 3. Headers are present before the actual HTTP request is made\n *\n * **Device ID Management:**\n * - First request: Generates a new device ID using nanoid\n * - Subsequent requests: Reuses the stored device ID\n * - Storage: Persisted in localStorage with cross-tab synchronization\n *\n * **Space ID Resolution:**\n * - The spaceIdProvider determines if the request requires space scoping\n * - Space ID is resolved before header injection\n * - Supports both space-scoped and non-scoped resources\n *\n * @example\n * ```typescript\n * import { Fetcher } from '@ahoo-wang/fetcher';\n * import { CoSecRequestInterceptor } from '@ahoo-wang/fetcher-cosec';\n * import { DeviceIdStorage } from '@ahoo-wang/fetcher-cosec';\n *\n * // Create dependencies\n * const deviceIdStorage = new DeviceIdStorage();\n *\n * // Create the interceptor\n * const coSecInterceptor = new CoSecRequestInterceptor({\n * appId: 'my-saas-application',\n * deviceIdStorage\n * });\n *\n * // Register with Fetcher\n * const fetcher = new Fetcher()\n * .addInterceptor(coSecInterceptor)\n * .setBaseUrl('https://api.example.com');\n *\n * // All requests will now include CoSec headers\n * const response = await fetcher.get('/api/users');\n * // Request headers include:\n * // - CoSec-App-Id: my-saas-application\n * // - CoSec-Device-Id: <unique-device-id>\n * // - CoSec-Request-Id: <unique-request-id>\n * ```\n *\n * @example\n * ```typescript\n * // Multi-tenant with space support\n * import { DefaultSpaceIdProvider, SpaceIdStorage } from '@ahoo-wang/fetcher-cosec';\n *\n * const spaceStorage = new SpaceIdStorage();\n * const spaceIdProvider = new DefaultSpaceIdProvider({\n * spacedResourcePredicate: {\n * test: (exchange) => exchange.request.url.includes('/spaces/')\n * },\n * spaceIdStorage: spaceStorage\n * });\n *\n * const coSecInterceptor = new CoSecRequestInterceptor({\n * appId: 'my-saas-application',\n * deviceIdStorage,\n * spaceIdProvider\n * });\n * ```\n */\nexport class CoSecRequestInterceptor implements RequestInterceptor {\n /**\n * The unique name of this interceptor.\n *\n * Used by the Fetcher interceptor system for identification and ordering.\n */\n readonly name = COSEC_REQUEST_INTERCEPTOR_NAME;\n\n /**\n * The execution order of this interceptor.\n *\n * Set to COSEC_REQUEST_INTERCEPTOR_ORDER to ensure proper positioning\n * in the interceptor chain.\n */\n readonly order = COSEC_REQUEST_INTERCEPTOR_ORDER;\n\n /**\n * The application identifier.\n *\n * This value is injected into the CoSec-App-Id header for every request.\n * It identifies your application in the CoSec authentication system.\n */\n private readonly appId: string;\n\n /**\n * Storage for device identifiers.\n *\n * Provides persistent storage and retrieval of device identifiers\n * with cross-tab synchronization.\n */\n private readonly deviceIdStorage: DeviceIdStorage;\n\n /**\n * Provider for resolving space identifiers.\n *\n * Determines which space a request belongs to (if any) for\n * multi-tenant, multi-space applications.\n */\n private readonly spaceIdProvider: SpaceIdProvider;\n\n /**\n * Creates a new CoSecRequestInterceptor instance.\n *\n * @param options - The CoSec configuration options\n * @param options.appId - The application identifier for CoSec authentication\n * @param options.deviceIdStorage - Storage for device identifier management\n * @param options.spaceIdProvider - Optional provider for space identification\n *\n * @throws Error if appId is empty or not provided\n * @throws Error if deviceIdStorage is not provided\n *\n * @example\n * ```typescript\n * // Basic instantiation\n * const interceptor = new CoSecRequestInterceptor({\n * appId: 'my-web-app',\n * deviceIdStorage: new DeviceIdStorage()\n * });\n *\n * // With custom device ID storage\n * const customStorage = new DeviceIdStorage({\n * key: 'custom-device-key',\n * storage: sessionStorage\n * });\n * const interceptorWithCustom = new CoSecRequestInterceptor({\n * appId: 'my-web-app',\n * deviceIdStorage: customStorage\n * });\n *\n * // With space support\n * const spaceStorage = new SpaceIdStorage();\n * const spaceProvider = new DefaultSpaceIdProvider({\n * spacedResourcePredicate: { test: (e) => true },\n * spaceIdStorage: spaceStorage\n * });\n * const interceptorWithSpace = new CoSecRequestInterceptor({\n * appId: 'my-web-app',\n * deviceIdStorage,\n * spaceIdProvider: spaceProvider\n * });\n * ```\n */\n constructor({\n appId,\n deviceIdStorage,\n spaceIdProvider,\n }: CoSecRequestOptions) {\n this.appId = appId;\n this.deviceIdStorage = deviceIdStorage;\n this.spaceIdProvider = spaceIdProvider ?? NoneSpaceIdProvider;\n }\n\n /**\n * Intercepts outgoing requests to add CoSec authentication headers.\n *\n * This method is called by the Fetcher interceptor chain for each request.\n * It enriches the request with security headers before the HTTP request is made.\n *\n * **Header Injection Process:**\n * 1. Generates a unique request ID using the idGenerator\n * 2. Retrieves or creates a device ID from deviceIdStorage\n * 3. Resolves space ID from spaceIdProvider (if applicable)\n * 4. Adds all headers to the request\n *\n * @param exchange - The fetch exchange containing the request to process\n *\n * @remarks\n * **Header Values:**\n * - `CoSec-App-Id`: Always set to the configured appId\n * - `CoSec-Device-Id`: Retrieved from storage or newly generated\n * - `CoSec-Request-Id`: Unique per request (not persisted)\n * - `CoSec-Space-Id`: Set only if spaceIdProvider returns a value\n *\n * **Error Handling:**\n * - If deviceIdStorage.getOrCreate() throws, the error propagates\n * - If spaceIdProvider.resolveSpaceId() throws, the error propagates\n * - If ensureRequestHeaders() fails, the error propagates\n *\n * **Thread Safety:**\n * - The idGenerator is safe for concurrent use\n * - deviceIdStorage handles concurrent access internally\n * - Each request gets a unique request ID\n *\n * @example\n * ```typescript\n * // The interceptor is called automatically by Fetcher\n * const fetcher = new Fetcher()\n * .addInterceptor(new CoSecRequestInterceptor({\n * appId: 'my-app',\n * deviceIdStorage: new DeviceIdStorage()\n * }));\n *\n * // This request will have CoSec headers added\n * const response = await fetcher.get('/api/data');\n * ```\n *\n * @example\n * ```typescript\n * // Headers added to outgoing request:\n * // GET /api/users HTTP/1.1\n * // Host: api.example.com\n * // CoSec-App-Id: my-app\n * // CoSec-Device-Id: abc123xyz789\n * // CoSec-Request-Id: req_abc123def456\n * // CoSec-Space-Id: workspace-alpha (if space resolved)\n * ```\n */\n async intercept(exchange: FetchExchange) {\n // Generate a unique request ID for this request\n const requestId = idGenerator.generateId();\n\n // Get or create a device ID\n const deviceId = this.deviceIdStorage.getOrCreate();\n\n // Resolve space ID for multi-tenant support\n const spaceId = this.spaceIdProvider.resolveSpaceId(exchange);\n\n // Ensure request headers object exists\n const requestHeaders = exchange.ensureRequestHeaders();\n\n // Add CoSec headers to the request\n requestHeaders[CoSecHeaders.APP_ID] = this.appId;\n requestHeaders[CoSecHeaders.DEVICE_ID] = deviceId;\n requestHeaders[CoSecHeaders.REQUEST_ID] = requestId;\n if (spaceId) {\n requestHeaders[CoSecHeaders.SPACE_ID] = spaceId;\n }\n }\n}\n","/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type {\n FetchExchange,\n RequestInterceptor} from '@ahoo-wang/fetcher';\nimport {\n DEFAULT_INTERCEPTOR_ORDER_STEP\n} from '@ahoo-wang/fetcher';\nimport {\n COSEC_REQUEST_INTERCEPTOR_ORDER,\n IGNORE_REFRESH_TOKEN_ATTRIBUTE_KEY,\n} from './cosecRequestInterceptor';\nimport type { JwtTokenManagerCapable } from './types';\nimport { CoSecHeaders } from './types';\n\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface AuthorizationInterceptorOptions\n extends JwtTokenManagerCapable {\n}\n\nexport const AUTHORIZATION_REQUEST_INTERCEPTOR_NAME =\n 'AuthorizationRequestInterceptor';\nexport const AUTHORIZATION_REQUEST_INTERCEPTOR_ORDER =\n COSEC_REQUEST_INTERCEPTOR_ORDER + DEFAULT_INTERCEPTOR_ORDER_STEP;\n\n/**\n * Request interceptor that automatically adds Authorization header to requests.\n *\n * This interceptor handles JWT token management by:\n * 1. Adding Authorization header with Bearer token if not already present\n * 2. Refreshing tokens when needed and possible\n * 3. Skipping refresh when explicitly requested via attributes\n *\n * The interceptor runs after CoSecRequestInterceptor but before FetchInterceptor in the chain.\n */\nexport class AuthorizationRequestInterceptor implements RequestInterceptor {\n readonly name = AUTHORIZATION_REQUEST_INTERCEPTOR_NAME;\n readonly order = AUTHORIZATION_REQUEST_INTERCEPTOR_ORDER;\n\n /**\n * Creates an AuthorizationRequestInterceptor instance.\n *\n * @param options - Configuration options containing the token manager\n */\n constructor(private readonly options: AuthorizationInterceptorOptions) {\n }\n\n /**\n * Intercepts the request exchange to add authorization headers.\n *\n * This method performs the following operations:\n * 1. Checks if a token exists and if Authorization header is already set\n * 2. Refreshes the token if needed, possible, and not explicitly ignored\n * 3. Adds the Authorization header with Bearer token if a token is available\n *\n * @param exchange - The fetch exchange containing request information\n * @returns Promise that resolves when the interception is complete\n */\n async intercept(exchange: FetchExchange): Promise<void> {\n // Get the current token from token manager\n let currentToken = this.options.tokenManager.currentToken;\n\n const requestHeaders = exchange.ensureRequestHeaders();\n\n // Skip if no token exists or Authorization header is already set\n if (!currentToken || requestHeaders[CoSecHeaders.AUTHORIZATION]) {\n return;\n }\n\n // Refresh token if needed and refreshable\n if (\n !exchange.attributes.has(IGNORE_REFRESH_TOKEN_ATTRIBUTE_KEY) &&\n currentToken.isRefreshNeeded &&\n currentToken.isRefreshable\n ) {\n await this.options.tokenManager.refresh();\n }\n\n // Get the current token again (might have been refreshed)\n currentToken = this.options.tokenManager.currentToken;\n\n // Add Authorization header if we have a token\n if (currentToken) {\n requestHeaders[CoSecHeaders.AUTHORIZATION] =\n `Bearer ${currentToken.access.token}`;\n }\n }\n}\n","/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { ResponseCodes } from './types';\nimport type { FetchExchange} from '@ahoo-wang/fetcher';\nimport { type ResponseInterceptor } from '@ahoo-wang/fetcher';\nimport type { AuthorizationInterceptorOptions } from './authorizationRequestInterceptor';\n\n/**\n * The name of the AuthorizationResponseInterceptor.\n */\nexport const AUTHORIZATION_RESPONSE_INTERCEPTOR_NAME =\n 'AuthorizationResponseInterceptor';\n\n/**\n * The order of the AuthorizationResponseInterceptor.\n * Set to a high negative value to ensure it runs early in the interceptor chain.\n */\nexport const AUTHORIZATION_RESPONSE_INTERCEPTOR_ORDER =\n Number.MIN_SAFE_INTEGER + 1000;\n\n/**\n * CoSecResponseInterceptor is responsible for handling unauthorized responses (401)\n * by attempting to refresh the authentication token and retrying the original request.\n *\n * This interceptor:\n * 1. Checks if the response status is 401 (UNAUTHORIZED)\n * 2. If so, and if there's a current token, attempts to refresh it\n * 3. On successful refresh, stores the new token and retries the original request\n * 4. On refresh failure, clears stored tokens and propagates the error\n */\nexport class AuthorizationResponseInterceptor implements ResponseInterceptor {\n readonly name = AUTHORIZATION_RESPONSE_INTERCEPTOR_NAME;\n readonly order = AUTHORIZATION_RESPONSE_INTERCEPTOR_ORDER;\n\n /**\n * Creates a new AuthorizationResponseInterceptor instance.\n * @param options - The CoSec configuration options including token storage and refresher\n */\n constructor(private options: AuthorizationInterceptorOptions) {}\n\n /**\n * Intercepts the response and handles unauthorized responses by refreshing tokens.\n * @param exchange - The fetch exchange containing request and response information\n */\n async intercept(exchange: FetchExchange): Promise<void> {\n const response = exchange.response;\n // If there's no response, nothing to intercept\n if (!response) {\n return;\n }\n\n // Only handle unauthorized responses (401)\n if (response.status !== ResponseCodes.UNAUTHORIZED) {\n return;\n }\n\n if (!this.options.tokenManager.isRefreshable) {\n return;\n }\n try {\n await this.options.tokenManager.refresh();\n // Retry the original request with the new token\n await exchange.fetcher.interceptors.exchange(exchange);\n } catch (error) {\n // If token refresh fails, clear stored tokens and re-throw the error\n this.options.tokenManager.tokenStorage.remove();\n throw error;\n }\n }\n}\n","/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { idGenerator } from './idGenerator';\nimport type { KeyStorageOptions} from '@ahoo-wang/fetcher-storage';\nimport { KeyStorage, typedIdentitySerializer } from '@ahoo-wang/fetcher-storage';\nimport {\n BroadcastTypedEventBus,\n SerialTypedEventBus,\n} from '@ahoo-wang/fetcher-eventbus';\n\nexport const DEFAULT_COSEC_DEVICE_ID_KEY = 'cosec-device-id';\n\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface DeviceIdStorageOptions\n extends Partial<KeyStorageOptions<string>> {\n}\n\n/**\n * Storage class for managing device identifiers.\n */\nexport class DeviceIdStorage extends KeyStorage<string> {\n constructor({\n key = DEFAULT_COSEC_DEVICE_ID_KEY,\n eventBus = new BroadcastTypedEventBus({\n delegate: new SerialTypedEventBus(DEFAULT_COSEC_DEVICE_ID_KEY),\n }),\n ...reset\n }: DeviceIdStorageOptions = {}) {\n super({ key, eventBus, ...reset, serializer: typedIdentitySerializer() });\n }\n\n /**\n * Generate a new device ID.\n *\n * @returns A newly generated device ID\n */\n generateDeviceId(): string {\n return idGenerator.generateId();\n }\n\n /**\n * Get or create a device ID.\n *\n * @returns The existing device ID if available, otherwise a newly generated one\n */\n getOrCreate(): string {\n // Try to get existing device ID from storage\n let deviceId = this.get();\n if (!deviceId) {\n // Generate a new device ID and store it\n deviceId = this.generateDeviceId();\n this.set(deviceId);\n }\n\n return deviceId;\n }\n}\n","/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { ErrorInterceptor, FetchExchange } from '@ahoo-wang/fetcher';\nimport { ResponseCodes } from './types';\n\n/**\n * The name identifier for the ForbiddenErrorInterceptor.\n * Used for interceptor registration and identification in the interceptor chain.\n */\nexport const FORBIDDEN_ERROR_INTERCEPTOR_NAME = 'ForbiddenErrorInterceptor';\n\n/**\n * The execution order for the ForbiddenErrorInterceptor.\n * Set to 0, indicating it runs at default priority in the interceptor chain.\n */\nexport const FORBIDDEN_ERROR_INTERCEPTOR_ORDER = 0;\n\n/**\n * Configuration options for the ForbiddenErrorInterceptor.\n */\nexport interface ForbiddenErrorInterceptorOptions {\n /**\n * Callback function invoked when a forbidden (403) response is detected.\n * This allows custom handling of authorization failures, such as displaying\n * permission error messages, redirecting to appropriate pages, or triggering\n * privilege escalation flows.\n *\n * @param exchange - The fetch exchange containing the request and response details\n * that resulted in the forbidden error\n * @returns Promise that resolves when the forbidden error handling is complete\n *\n * @example\n * ```typescript\n * const options: ForbiddenErrorInterceptorOptions = {\n * onForbidden: async (exchange) => {\n * console.log('Access forbidden for:', exchange.request.url);\n * // Show permission error or redirect\n * showPermissionError('You do not have permission to access this resource');\n * }\n * };\n * ```\n */\n onForbidden: (exchange: FetchExchange) => Promise<void>;\n}\n\n/**\n * An error interceptor that handles HTTP 403 Forbidden responses by invoking a custom callback.\n *\n * This interceptor is designed to provide centralized handling of authorization failures\n * across all HTTP requests. When a response with status code 403 is encountered, it calls\n * the configured `onForbidden` callback, allowing applications to implement custom\n * authorization recovery logic such as:\n * - Displaying permission error messages\n * - Redirecting users to access request pages\n * - Triggering privilege escalation workflows\n * - Logging security events\n * - Showing upgrade prompts for premium features\n *\n * The interceptor does not modify the response or retry requests automatically - it delegates\n * all handling to the provided callback function. This allows for flexible, application-specific\n * handling of forbidden access scenarios.\n *\n * @example\n * ```typescript\n * // Basic usage with error display\n * const interceptor = new ForbiddenErrorInterceptor({\n * onForbidden: async (exchange) => {\n * console.log('Forbidden access detected for:', exchange.request.url);\n * showErrorToast('You do not have permission to access this resource');\n * }\n * });\n *\n * fetcher.interceptors.error.use(interceptor);\n * ```\n *\n * @example\n * ```typescript\n * // Advanced usage with role-based handling\n * const interceptor = new ForbiddenErrorInterceptor({\n * onForbidden: async (exchange) => {\n * const userRole = getCurrentUserRole();\n *\n * if (userRole === 'guest') {\n * // Redirect to login for guests\n * redirectToLogin(exchange.request.url);\n * } else if (userRole === 'user') {\n * // Show upgrade prompt for basic users\n * showUpgradePrompt('Upgrade to premium for access to this feature');\n * } else {\n * // Log security event for authenticated users\n * logSecurityEvent('Forbidden access attempt', {\n * url: exchange.request.url,\n * userId: getCurrentUserId(),\n * timestamp: new Date().toISOString()\n * });\n * showErrorToast('Access denied due to insufficient permissions');\n * }\n * }\n * });\n * ```\n */\nexport class ForbiddenErrorInterceptor implements ErrorInterceptor {\n /**\n * The unique name identifier for this interceptor instance.\n * Used for registration, debugging, and interceptor chain management.\n */\n readonly name = FORBIDDEN_ERROR_INTERCEPTOR_NAME;\n\n /**\n * The execution order priority for this interceptor in the error interceptor chain.\n * Lower values execute earlier in the chain. Default priority (0) allows other\n * interceptors to run first if needed.\n */\n readonly order = FORBIDDEN_ERROR_INTERCEPTOR_ORDER;\n\n /**\n * Creates a new ForbiddenErrorInterceptor instance.\n *\n * @param options - Configuration options containing the callback to handle forbidden responses.\n * Must include the `onForbidden` callback function.\n *\n * @throws Will throw an error if options are not provided or if `onForbidden` callback is missing.\n *\n * @example\n * ```typescript\n * const interceptor = new ForbiddenErrorInterceptor({\n * onForbidden: async (exchange) => {\n * // Handle forbidden access\n * }\n * });\n * ```\n */\n constructor(private options: ForbiddenErrorInterceptorOptions) {}\n\n /**\n * Intercepts fetch exchanges to detect and handle forbidden (403) responses.\n *\n * This method examines the response status code and invokes the configured `onForbidden`\n * callback when a 403 Forbidden response is detected. The method is asynchronous to\n * allow the callback to perform async operations like API calls, redirects, or UI updates.\n *\n * The interceptor only acts on responses with status code 403. Other error codes are\n * ignored and passed through to other error interceptors in the chain.\n *\n * @param exchange - The fetch exchange containing request, response, and error information\n * to be inspected for forbidden status codes. The exchange object provides\n * access to the original request, response details, and any error information.\n * @returns Promise that resolves when the forbidden error handling is complete.\n * Returns void - the method does not modify the exchange or return values.\n *\n * @remarks\n * - Only responds to HTTP 403 status codes\n * - Does not retry requests or modify responses\n * - Allows async operations in the callback\n * - Does not throw exceptions - delegates all error handling to the callback\n * - Safe to use with other error interceptors\n *\n * @example\n * ```typescript\n * // The intercept method is called automatically by the fetcher\n * // No manual invocation needed - this is for documentation purposes\n * const interceptor = new ForbiddenErrorInterceptor({\n * onForbidden: async (exchange) => {\n * // exchange.response.status === 403\n * // exchange.request contains original request details\n * await handleForbiddenAccess(exchange);\n * }\n * });\n * ```\n */\n async intercept(exchange: FetchExchange): Promise<void> {\n // Check if the response status indicates forbidden access (403)\n if (exchange.response?.status === ResponseCodes.FORBIDDEN) {\n // Invoke the custom forbidden error handler\n // Allow the callback to perform async operations\n await this.options.onForbidden(exchange);\n }\n }\n}\n","/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { TokenStorage } from './tokenStorage';\nimport type { TokenRefresher } from './tokenRefresher';\nimport type { JwtCompositeToken, RefreshTokenStatusCapable } from './jwtToken';\nimport { FetcherError } from '@ahoo-wang/fetcher';\n\nexport class RefreshTokenError extends FetcherError {\n constructor(\n public readonly token: JwtCompositeToken,\n cause?: Error | any,\n ) {\n super(`Refresh token failed.`, cause);\n this.name = 'RefreshTokenError';\n Object.setPrototypeOf(this, RefreshTokenError.prototype);\n }\n}\n\n/**\n * Manages JWT token refreshing operations and provides status information\n */\nexport class JwtTokenManager implements RefreshTokenStatusCapable {\n private refreshInProgress?: Promise<void>;\n\n /**\n * Creates a new JwtTokenManager instance\n * @param tokenStorage The storage used to persist tokens\n * @param tokenRefresher The refresher used to refresh expired tokens\n */\n constructor(\n public readonly tokenStorage: TokenStorage,\n public readonly tokenRefresher: TokenRefresher,\n ) {}\n\n /**\n * Gets the current JWT composite token from storage\n * @returns The current token or null if none exists\n */\n get currentToken(): JwtCompositeToken | null {\n return this.tokenStorage.get();\n }\n\n /**\n * Refreshes the JWT token\n * @returns Promise that resolves when refresh is complete\n * @throws Error if no token is found or refresh fails\n */\n async refresh(): Promise<void> {\n const jwtToken = this.currentToken;\n if (!jwtToken) {\n throw new Error('No token found');\n }\n if (this.refreshInProgress) {\n return this.refreshInProgress;\n }\n\n this.refreshInProgress = this.tokenRefresher\n .refresh(jwtToken.token)\n .then(newToken => {\n this.tokenStorage.setCompositeToken(newToken);\n })\n .catch(error => {\n this.tokenStorage.remove();\n throw new RefreshTokenError(jwtToken, error);\n })\n .finally(() => {\n this.refreshInProgress = undefined;\n });\n\n return this.refreshInProgress;\n }\n\n /**\n * Indicates if the current token needs to be refreshed\n * @returns true if the access token is expired and needs refresh, false otherwise\n */\n get isRefreshNeeded(): boolean {\n if (!this.currentToken) {\n return false;\n }\n return this.currentToken.isRefreshNeeded;\n }\n\n /**\n * Indicates if the current token can be refreshed\n * @returns true if the refresh token is still valid, false otherwise\n */\n get isRefreshable(): boolean {\n if (!this.currentToken) {\n return false;\n }\n return this.currentToken.isRefreshable;\n }\n}\n","/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type {\n FetchExchange,\n RequestInterceptor} from '@ahoo-wang/fetcher';\nimport {\n DEFAULT_INTERCEPTOR_ORDER_STEP, URL_RESOLVE_INTERCEPTOR_ORDER,\n} from '@ahoo-wang/fetcher';\nimport type { TokenStorage } from './tokenStorage';\n\nconst TENANT_ID_PATH_KEY = 'tenantId';\nconst OWNER_ID_PATH_KEY = 'ownerId';\n\n/**\n * Configuration options for resource attribution\n */\nexport interface ResourceAttributionOptions {\n /**\n * The path parameter key used for tenant ID in URL templates\n */\n tenantId?: string;\n /**\n * The path parameter key used for owner ID in URL templates\n */\n ownerId?: string;\n /**\n * Storage mechanism for retrieving current authentication tokens\n */\n tokenStorage: TokenStorage;\n}\n\n/**\n * Name identifier for the ResourceAttributionRequestInterceptor\n */\nexport const RESOURCE_ATTRIBUTION_REQUEST_INTERCEPTOR_NAME =\n 'ResourceAttributionRequestInterceptor';\n/**\n * Order priority for the ResourceAttributionRequestInterceptor, set to maximum safe integer to ensure it runs last\n */\nexport const RESOURCE_ATTRIBUTION_REQUEST_INTERCEPTOR_ORDER =\n URL_RESOLVE_INTERCEPTOR_ORDER - DEFAULT_INTERCEPTOR_ORDER_STEP;\n\n/**\n * Request interceptor that automatically adds tenant and owner ID path parameters to requests\n * based on the current authentication token. This is useful for multi-tenant applications where\n * requests need to include tenant-specific information in the URL path.\n */\nexport class ResourceAttributionRequestInterceptor\n implements RequestInterceptor {\n readonly name = RESOURCE_ATTRIBUTION_REQUEST_INTERCEPTOR_NAME;\n readonly order = RESOURCE_ATTRIBUTION_REQUEST_INTERCEPTOR_ORDER;\n private readonly tenantIdPathKey: string;\n private readonly ownerIdPathKey: string;\n private readonly tokenStorage: TokenStorage;\n\n /**\n * Creates a new ResourceAttributionRequestInterceptor\n * @param options - Configuration options for resource attribution including tenantId, ownerId and tokenStorage\n */\n constructor({\n tenantId = TENANT_ID_PATH_KEY,\n ownerId = OWNER_ID_PATH_KEY,\n tokenStorage,\n }: ResourceAttributionOptions) {\n this.tenantIdPathKey = tenantId;\n this.ownerIdPathKey = ownerId;\n this.tokenStorage = tokenStorage;\n }\n\n /**\n * Intercepts outgoing requests and automatically adds tenant and owner ID path parameters\n * if they are defined in the URL template but not provided in the request.\n * @param exchange - The fetch exchange containing the request information\n */\n intercept(exchange: FetchExchange): void {\n const currentToken = this.tokenStorage.get();\n if (!currentToken) {\n return;\n }\n const principal = currentToken.access.payload;\n if (!principal) {\n return;\n }\n if (!principal.tenantId && !principal.sub) {\n return;\n }\n\n // Extract path parameters from the URL template\n const extractedPathParams =\n exchange.fetcher.urlBuilder.urlTemplateResolver.extractPathParams(\n exchange.request.url,\n );\n const tenantIdPathKey = this.tenantIdPathKey;\n const requestPathParams = exchange.ensureRequestUrlParams().path;\n const tenantId = principal.tenantId;\n\n // Add tenant ID to path parameters if it's part of the URL template and not already provided\n if (\n tenantId &&\n extractedPathParams.includes(tenantIdPathKey) &&\n !requestPathParams[tenantIdPathKey]\n ) {\n requestPathParams[tenantIdPathKey] = tenantId;\n }\n\n const ownerIdPathKey = this.ownerIdPathKey;\n const ownerId = principal.sub;\n\n // Add owner ID to path parameters if it's part of the URL template and not already provided\n if (\n ownerId &&\n extractedPathParams.includes(ownerIdPathKey) &&\n !requestPathParams[ownerIdPathKey]\n ) {\n requestPathParams[ownerIdPathKey] = ownerId;\n }\n }\n}\n","/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n/**\n * Interface representing a JWT payload as defined in RFC 7519.\n * Contains standard JWT claims as well as custom properties.\n */\nexport interface JwtPayload {\n /**\n * JWT ID - provides a unique identifier for the JWT.\n */\n jti: string;\n /**\n * Subject - identifies the principal that is the subject of the JWT.\n */\n sub: string;\n /**\n * Issuer - identifies the principal that issued the JWT.\n */\n iss?: string;\n /**\n * Audience - identifies the recipients that the JWT is intended for.\n * Can be a single string or an array of strings.\n */\n aud?: string | string[];\n /**\n * Expiration Time - identifies the expiration time on or after which the JWT MUST NOT be accepted for processing.\n * Represented as NumericDate (seconds since Unix epoch).\n */\n exp: number;\n /**\n * Not Before - identifies the time before which the JWT MUST NOT be accepted for processing.\n * Represented as NumericDate (seconds since Unix epoch).\n */\n nbf?: number;\n /**\n * Issued At - identifies the time at which the JWT was issued.\n * Represented as NumericDate (seconds since Unix epoch).\n */\n iat: number;\n\n /**\n * Allows additional custom properties to be included in the payload.\n */\n [key: string]: any;\n}\n\n/**\n * Interface representing a JWT payload with CoSec-specific extensions.\n * Extends the standard JwtPayload interface with additional CoSec-specific properties.\n */\nexport interface CoSecJwtPayload extends JwtPayload {\n /**\n * Tenant identifier - identifies the tenant scope for the JWT.\n */\n tenantId?: string;\n /**\n * Policies - array of policy identifiers associated with the JWT.\n * These are security policies defined internally by Cosec.\n */\n policies?: string[];\n /**\n * Roles - array of role identifiers associated with the JWT.\n * Role IDs indicate what roles the token belongs to.\n */\n roles?: string[];\n /**\n * Attributes - custom key-value pairs providing additional information about the JWT.\n */\n attributes?: Record<string, any>;\n}\n\n/**\n * Parses a JWT token and extracts its payload.\n *\n * This function decodes the payload part of a JWT token, handling Base64URL decoding\n * and JSON parsing. It validates the token structure and returns null for invalid tokens.\n *\n * @param token - The JWT token string to parse\n * @returns The parsed JWT payload or null if parsing fails\n */\nexport function parseJwtPayload<T extends JwtPayload>(token: string): T | null {\n try {\n if (typeof token !== 'string') {\n return null;\n }\n const parts = token.split('.');\n if (parts.length !== 3) {\n return null;\n }\n\n const base64Url = parts[1];\n const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');\n\n // Add padding if needed\n const paddedBase64 = base64.padEnd(\n base64.length + ((4 - (base64.length % 4)) % 4),\n '=',\n );\n\n const jsonPayload = decodeURIComponent(\n atob(paddedBase64)\n .split('')\n .map(function (c) {\n return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);\n })\n .join(''),\n );\n return JSON.parse(jsonPayload) as T;\n } catch (error) {\n // Avoid exposing sensitive information in error logs\n console.error('Failed to parse JWT token', error);\n return null;\n }\n}\n\nexport interface EarlyPeriodCapable {\n /**\n * The time in seconds before actual expiration when the token should be considered expired (default: 0)\n */\n readonly earlyPeriod: number;\n}\n\n/**\n * Checks if a JWT token is expired based on its expiration time (exp claim).\n *\n * This function determines if a JWT token has expired by comparing its exp claim\n * with the current time. If the token is a string, it will be parsed first.\n * Tokens without an exp claim are considered not expired.\n *\n * The early period parameter allows for early token expiration, which is useful\n * for triggering token refresh before the token actually expires. This helps\n * avoid race conditions where a token expires between the time it is checked and\n * the time it is used.\n *\n * @param token - The JWT token to check, either as a string or as a JwtPayload object\n * @param earlyPeriod - The time in seconds before actual expiration when the token should be considered expired (default: 0)\n * @returns true if the token is expired (or will expire within the early period) or cannot be parsed, false otherwise\n */\nexport function isTokenExpired(\n token: string | CoSecJwtPayload,\n earlyPeriod: number = 0,\n): boolean {\n const payload = typeof token === 'string' ? parseJwtPayload(token) : token;\n if (!payload) {\n return true;\n }\n\n const expAt = payload.exp;\n if (!expAt) {\n return false;\n }\n\n const now = Date.now() / 1000;\n return now > expAt - earlyPeriod;\n}\n","/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type {\n CoSecJwtPayload,\n EarlyPeriodCapable,\n JwtPayload} from './jwts';\nimport {\n isTokenExpired,\n parseJwtPayload,\n} from './jwts';\nimport type { CompositeToken } from './tokenRefresher';\nimport type { Serializer } from '@ahoo-wang/fetcher-storage';\n\n/**\n * Interface for JWT token with typed payload.\n *\n * This interface represents a JWT token that includes the raw token string,\n * a parsed payload of a specific type, and methods to check expiration status.\n * It extends EarlyPeriodCapable to support early expiration checks.\n *\n * @template Payload The type of the JWT payload, must extend JwtPayload\n *\n * @example\n * ```typescript\n * interface CustomPayload extends JwtPayload {\n * userId: string;\n * roles: string[];\n * }\n *\n * const token: IJwtToken<CustomPayload> = new JwtToken<CustomPayload>('jwt.token.here');\n * if (!token.isExpired) {\n * console.log(token.payload?.userId);\n * }\n * ```\n */\nexport interface IJwtToken<\n Payload extends JwtPayload,\n> extends EarlyPeriodCapable {\n /**\n * The raw JWT token string.\n */\n readonly token: string;\n\n /**\n * The parsed JWT payload. Null if the token could not be parsed.\n */\n readonly payload: Payload | null;\n\n /**\n * Indicates whether the token is expired, considering the early period.\n */\n isExpired: boolean;\n}\n\n/**\n * Class representing a JWT token with typed payload.\n *\n * This class provides a concrete implementation of IJwtToken, parsing the JWT\n * token string and providing access to the payload and expiration status.\n * It supports early expiration periods for proactive token refresh.\n *\n * @template Payload The type of the JWT payload, must extend JwtPayload\n *\n * @example\n * ```typescript\n * const token = new JwtToken<CoSecJwtPayload>('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...', 300000); // 5 min early period\n * console.log(token.isExpired); // false if not expired\n * console.log(token.payload?.sub); // user ID from payload\n * ```\n */\nexport class JwtToken<\n Payload extends JwtPayload,\n> implements IJwtToken<Payload> {\n /**\n * The parsed JWT payload. Null if the token could not be parsed.\n */\n public readonly payload: Payload | null;\n\n /**\n * Creates a new JwtToken instance.\n *\n * Parses the JWT token string to extract the payload and stores the early period\n * for expiration checks.\n *\n * @param token The raw JWT token string to parse\n * @param earlyPeriod The early expiration period in milliseconds (default: 0).\n * Tokens are considered expired this many milliseconds before their actual expiration time.\n *\n * @throws Will not throw but payload will be null if token parsing fails\n */\n constructor(\n public readonly token: string,\n public readonly earlyPeriod: number = 0,\n ) {\n this.payload = parseJwtPayload<Payload>(token);\n }\n\n /**\n * Checks if the token is expired.\n *\n * Considers both the token's expiration time and the early period.\n * Returns true if the payload is null (parsing failed) or if the token is expired.\n *\n * @returns true if the token is expired or invalid, false otherwise\n */\n get isExpired(): boolean {\n if (!this.payload) {\n return true;\n }\n return isTokenExpired(this.payload, this.earlyPeriod);\n }\n}\n\n/**\n * Interface for checking refresh token status capabilities.\n *\n * This interface defines methods to check if token refresh is needed and possible.\n */\nexport interface RefreshTokenStatusCapable {\n /**\n * Checks if the access token needs to be refreshed.\n *\n * @returns true if the access token is expired, false otherwise\n */\n readonly isRefreshNeeded: boolean;\n\n /**\n * Checks if the refresh token is still valid and can be used to refresh the access token.\n *\n * @returns true if the refresh token is not expired, false otherwise\n */\n readonly isRefreshable: boolean;\n}\n\n/**\n * Class representing a composite token containing both access and refresh tokens.\n *\n * This class manages both access and refresh JWT tokens together, providing\n * convenient methods to check authentication status, refresh needs, and user information.\n * It implements both EarlyPeriodCapable and RefreshTokenStatusCapable interfaces.\n *\n * @example\n * ```typescript\n * const compositeToken = new JwtCompositeToken({\n * accessToken: 'access.jwt.here',\n * refreshToken: 'refresh.jwt.here'\n * }, 300000); // 5 min early period\n *\n * if (compositeToken.authenticated) {\n * console.log(compositeToken.currentUser?.sub);\n * }\n *\n * if (compositeToken.isRefreshNeeded && compositeToken.isRefreshable) {\n * // Refresh the access token\n * }\n * ```\n */\nexport class JwtCompositeToken\n implements EarlyPeriodCapable, RefreshTokenStatusCapable {\n /**\n * The access JWT token instance.\n */\n public readonly access: JwtToken<CoSecJwtPayload>;\n\n /**\n * The refresh JWT token instance.\n */\n public readonly refresh: JwtToken<JwtPayload>;\n\n /**\n * Creates a new JwtCompositeToken instance.\n *\n * Initializes both access and refresh token instances with the provided early period.\n *\n * @param token The composite token containing access and refresh token strings\n * @param earlyPeriod The early expiration period in milliseconds (default: 0)\n */\n constructor(\n public readonly token: CompositeToken,\n public readonly earlyPeriod: number = 0,\n ) {\n this.access = new JwtToken(token.accessToken, earlyPeriod);\n this.refresh = new JwtToken(token.refreshToken, earlyPeriod);\n }\n\n /**\n * Checks if the access token needs to be refreshed.\n *\n * @returns true if the access token is expired, false otherwise\n */\n get isRefreshNeeded(): boolean {\n return this.access.isExpired;\n }\n\n /**\n * Checks if the refresh token is still valid and can be used to refresh the access token.\n *\n * @returns true if the refresh token is not expired, false otherwise\n */\n get isRefreshable(): boolean {\n return !this.refresh.isExpired;\n }\n\n /**\n * Checks if the user is currently authenticated (access token is valid).\n *\n * @returns true if the access token is not expired, false otherwise\n */\n get authenticated(): boolean {\n return !this.access.isExpired;\n }\n}\n\n/**\n * Serializer for JwtCompositeToken that handles conversion to and from JSON strings.\n *\n * This class provides serialization and deserialization functionality for JwtCompositeToken\n * instances, allowing them to be stored and retrieved from persistent storage.\n *\n * @example\n * ```typescript\n * const serializer = new JwtCompositeTokenSerializer(300000);\n * const token = new JwtCompositeToken({ accessToken: '...', refreshToken: '...' });\n *\n * const serialized = serializer.serialize(token);\n * const deserialized = serializer.deserialize(serialized);\n * ```\n */\nexport class JwtCompositeTokenSerializer\n implements Serializer<string, JwtCompositeToken>, EarlyPeriodCapable {\n /**\n * Creates a new JwtCompositeTokenSerializer instance.\n *\n * @param earlyPeriod The early expiration period in milliseconds to use for deserialized tokens (default: 0)\n */\n constructor(public readonly earlyPeriod: number = 0) {\n }\n\n /**\n * Deserializes a JSON string to a JwtCompositeToken.\n *\n * Parses the JSON string and creates a new JwtCompositeToken instance with the stored tokens.\n *\n * @param value The JSON string representation of a composite token\n * @returns A JwtCompositeToken instance\n * @throws SyntaxError if the JSON string is invalid\n * @throws Error if the parsed object doesn't match the expected CompositeToken structure\n */\n deserialize(value: string): JwtCompositeToken {\n const compositeToken = JSON.parse(value) as CompositeToken;\n return new JwtCompositeToken(compositeToken, this.earlyPeriod);\n }\n\n /**\n * Serializes a JwtCompositeToken to a JSON string.\n *\n * Converts the composite token to a JSON string for storage.\n *\n * @param value The JwtCompositeToken to serialize\n * @returns A JSON string representation of the composite token\n */\n serialize(value: JwtCompositeToken): string {\n return JSON.stringify(value.token);\n }\n}\n\n/**\n * Default instance of JwtCompositeTokenSerializer with no early period.\n *\n * This pre-configured serializer can be used directly for basic serialization needs.\n */\nexport const jwtCompositeTokenSerializer = new JwtCompositeTokenSerializer();\n","/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { JwtCompositeToken, JwtCompositeTokenSerializer } from './jwtToken';\nimport type { CompositeToken } from './tokenRefresher';\nimport type { CoSecJwtPayload, EarlyPeriodCapable } from './jwts';\nimport type { KeyStorageOptions } from '@ahoo-wang/fetcher-storage';\nimport { KeyStorage } from '@ahoo-wang/fetcher-storage';\nimport {\n BroadcastTypedEventBus,\n SerialTypedEventBus,\n} from '@ahoo-wang/fetcher-eventbus';\n\n/**\n * Default key used for storing CoSec tokens in storage.\n */\nexport const DEFAULT_COSEC_TOKEN_KEY = 'cosec-token';\n\n/**\n * Options for configuring TokenStorage.\n * Extends KeyStorageOptions excluding 'serializer' and includes EarlyPeriodCapable properties.\n */\nexport interface TokenStorageOptions\n extends\n Partial<Omit<KeyStorageOptions<JwtCompositeToken>, 'serializer'>>,\n Partial<EarlyPeriodCapable> {}\n\n/**\n * Storage class for managing access and refresh tokens in CoSec authentication.\n * Provides methods to store, retrieve, and manage JWT composite tokens with early period support.\n * Extends KeyStorage to handle persistence and implements EarlyPeriodCapable for token refresh timing.\n */\nexport class TokenStorage\n extends KeyStorage<JwtCompositeToken>\n implements EarlyPeriodCapable\n{\n /**\n * The early period in milliseconds for token refresh timing.\n */\n public readonly earlyPeriod: number;\n\n /**\n * Creates a new TokenStorage instance.\n * @param options - Configuration options for the token storage.\n * @param options.key - The storage key for tokens. Defaults to DEFAULT_COSEC_TOKEN_KEY.\n * @param options.eventBus - Event bus for token change notifications. Defaults to a BroadcastTypedEventBus with SerialTypedEventBus delegate.\n * @param options.earlyPeriod - Early period for token refresh in milliseconds. Defaults to 0.\n * @param reset - Additional options passed to KeyStorage.\n */\n constructor({\n key = DEFAULT_COSEC_TOKEN_KEY,\n eventBus = new BroadcastTypedEventBus({\n delegate: new SerialTypedEventBus(DEFAULT_COSEC_TOKEN_KEY),\n }),\n earlyPeriod = 0,\n ...reset\n }: TokenStorageOptions = {}) {\n super({\n key,\n eventBus,\n ...reset,\n serializer: new JwtCompositeTokenSerializer(earlyPeriod),\n });\n this.earlyPeriod = earlyPeriod;\n }\n\n /**\n * Sets a composite token in storage.\n * Converts the composite token to a JwtCompositeToken and stores it.\n * @deprecated Use signIn() instead for better semantic clarity.\n * @param compositeToken - The composite token containing access and refresh tokens.\n */\n setCompositeToken(compositeToken: CompositeToken) {\n this.signIn(compositeToken);\n }\n\n /**\n * Signs in by storing the composite token.\n * @param compositeToken - The composite token to store for authentication.\n */\n signIn(compositeToken: CompositeToken): void {\n this.set(new JwtCompositeToken(compositeToken, this.earlyPeriod));\n }\n\n /**\n * Signs out by removing the stored token.\n * Clears the token from storage.\n */\n signOut(): void {\n this.remove();\n }\n\n /**\n * Checks if the user is authenticated.\n * @returns true if a valid token is present and authenticated, false otherwise.\n */\n get authenticated(): boolean {\n return this.get()?.authenticated === true;\n }\n\n /**\n * Gets the current user's JWT payload.\n * @returns The JWT payload of the current user if authenticated, null otherwise.\n */\n get currentUser(): CoSecJwtPayload | null {\n if (!this.authenticated) {\n return null;\n }\n return this.get()?.access.payload ?? null;\n }\n}\n","/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport type { ErrorInterceptor, FetchExchange } from '@ahoo-wang/fetcher';\nimport { ResponseCodes } from './types';\nimport { RefreshTokenError } from './jwtTokenManager';\n\n/**\n * The name identifier for the UnauthorizedErrorInterceptor.\n * Used for interceptor registration and identification in the interceptor chain.\n */\nexport const UNAUTHORIZED_ERROR_INTERCEPTOR_NAME =\n 'UnauthorizedErrorInterceptor';\n\n/**\n * The execution order for the UnauthorizedErrorInterceptor.\n * Set to 0, indicating it runs at default priority in the interceptor chain.\n */\nexport const UNAUTHORIZED_ERROR_INTERCEPTOR_ORDER = 0;\n\n/**\n * Configuration options for the UnauthorizedErrorInterceptor.\n */\nexport interface UnauthorizedErrorInterceptorOptions {\n /**\n * Callback function invoked when an unauthorized (401) response is detected.\n * This allows custom handling of authentication failures, such as redirecting to login\n * or triggering token refresh mechanisms.\n *\n * @param exchange - The fetch exchange containing the request and response details\n * that resulted in the unauthorized error\n */\n onUnauthorized: (exchange: FetchExchange) => Promise<void> | void;\n}\n\n/**\n * An error interceptor that handles HTTP 401 Unauthorized responses by invoking a custom callback.\n *\n * This interceptor is designed to provide centralized handling of authentication failures\n * across all HTTP requests. When a response with status code 401 is encountered, it calls\n * the configured `onUnauthorized` callback, allowing applications to implement custom\n * authentication recovery logic such as:\n * - Redirecting users to login pages\n * - Triggering token refresh flows\n * - Clearing stored authentication state\n * - Displaying authentication error messages\n *\n * The interceptor does not modify the response or retry requests automatically - it delegates\n * all handling to the provided callback function.\n *\n * @example\n * ```typescript\n * const interceptor = new UnauthorizedErrorInterceptor({\n * onUnauthorized: (exchange) => {\n * console.log('Unauthorized access detected for:', exchange.request.url);\n * // Redirect to login page or refresh token\n * window.location.href = '/login';\n * }\n * });\n *\n * fetcher.interceptors.error.use(interceptor);\n * ```\n */\nexport class UnauthorizedErrorInterceptor implements ErrorInterceptor {\n /**\n * The unique name identifier for this interceptor instance.\n */\n readonly name = UNAUTHORIZED_ERROR_INTERCEPTOR_NAME;\n\n /**\n * The execution order priority for this interceptor in the chain.\n */\n readonly order = UNAUTHORIZED_ERROR_INTERCEPTOR_ORDER;\n\n /**\n * Creates a new UnauthorizedErrorInterceptor instance.\n *\n * @param options - Configuration options containing the callback to handle unauthorized responses\n */\n constructor(private options: UnauthorizedErrorInterceptorOptions) {}\n\n /**\n * Intercepts fetch exchanges to detect and handle unauthorized (401) responses\n * and RefreshTokenError exceptions.\n *\n * This method checks if the response status is 401 (Unauthorized) or if the exchange\n * contains an error of type `RefreshTokenError`. If either condition is met, it invokes\n * the configured `onUnauthorized` callback with the exchange details. The method\n * does not return a value or throw exceptions - all error handling is delegated\n * to the callback function.\n *\n * @param exchange - The fetch exchange containing request, response, and error information\n * to be inspected for unauthorized status codes or refresh token errors\n * @returns {void} This method does not return a value\n *\n * @example\n * ```typescript\n * const interceptor = new UnauthorizedErrorInterceptor({\n * onUnauthorized: (exchange) => {\n * // Custom logic here\n * }\n * });\n * ```\n */\n async intercept(exchange: FetchExchange): Promise<void> {\n if (\n exchange.response?.status === ResponseCodes.UNAUTHORIZED ||\n exchange.error instanceof RefreshTokenError\n ) {\n await this.options.onUnauthorized(exchange);\n }\n }\n}\n","/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { Fetcher, FetcherConfigurer, FetchExchange } from '@ahoo-wang/fetcher';\nimport { AuthorizationRequestInterceptor } from './authorizationRequestInterceptor';\nimport { AuthorizationResponseInterceptor } from './authorizationResponseInterceptor';\nimport { CoSecRequestInterceptor } from './cosecRequestInterceptor';\nimport { DeviceIdStorage } from './deviceIdStorage';\nimport { ForbiddenErrorInterceptor } from './forbiddenErrorInterceptor';\nimport { JwtTokenManager } from './jwtTokenManager';\nimport { ResourceAttributionRequestInterceptor } from './resourceAttributionRequestInterceptor';\nimport type { TokenRefresher } from './tokenRefresher';\nimport { TokenStorage } from './tokenStorage';\nimport { UnauthorizedErrorInterceptor } from './unauthorizedErrorInterceptor';\nimport type { AppIdCapable, DeviceIdStorageCapable } from './types';\nimport type { SpaceIdProvider } from './spaceIdProvider';\nimport { NoneSpaceIdProvider } from './spaceIdProvider';\n\n/**\n * Configuration interface for CoSec security features.\n *\n * This interface provides a simplified, flexible configuration for setting up\n * CoSec authentication and security features. It supports both full JWT\n * authentication setups and minimal header injection configurations.\n *\n * @remarks\n * **Required Properties:**\n * - appId: Your application's unique identifier in the CoSec system\n *\n * **Optional Properties:**\n * - tokenStorage: Custom token persistence (defaults to TokenStorage)\n * - deviceIdStorage: Custom device ID management (defaults to DeviceIdStorage)\n * - tokenRefresher: JWT token refresh handler (enables authentication)\n * - spaceIdProvider: Space identifier resolver (enables multi-tenant)\n * - onUnauthorized: Custom 401 error handler\n * - onForbidden: Custom 403 error handler\n *\n * **Configuration Levels:**\n * - Minimal: Only appId (headers only, no authentication)\n * - Standard: appId + tokenRefresher (full JWT auth)\n * - Enterprise: All options (multi-tenant with custom handlers)\n *\n * @example\n * ```typescript\n * // Minimal: Headers only, no authentication\n * const minimalConfig: CoSecConfig = {\n * appId: 'my-web-app'\n * };\n *\n * // Standard: Full JWT authentication\n * const standardConfig: CoSecConfig = {\n * appId: 'my-web-app',\n * tokenRefresher: {\n * refresh: async (token) => refreshMyToken(token)\n * },\n * onUnauthorized: () => window.location.href = '/login',\n * onForbidden: () => showError('Access denied')\n * };\n *\n * // Enterprise: Multi-tenant with custom storage\n * const enterpriseConfig: CoSecConfig = {\n * appId: 'my-saas-app',\n * tokenStorage: myCustomTokenStorage,\n * deviceIdStorage: myCustomDeviceStorage,\n * tokenRefresher: myTokenRefresher,\n * spaceIdProvider: mySpaceProvider,\n * onUnauthorized: handle401,\n * onForbidden: handle403\n * };\n * ```\n */\nexport interface CoSecConfig\n extends AppIdCapable, Partial<DeviceIdStorageCapable> {\n /**\n * Your application's unique identifier in the CoSec authentication system.\n *\n * This ID is sent with every request in the `CoSec-App-Id` header and is\n * used to identify which application is making the request. Obtain this\n * value from your CoSec administration console.\n *\n * @remarks\n * **Requirements:**\n * - Must be a non-empty string\n * - Must be registered in the CoSec system\n * - Should be unique per application/deployment\n *\n * **Usage:**\n * ```typescript\n * const config: CoSecConfig = {\n * appId: 'my-production-app-001' // Registered app ID\n * };\n * ```\n */\n readonly appId: string;\n\n /**\n * Custom implementation for storing and retrieving JWT tokens.\n *\n * If not provided, a default TokenStorage instance will be created\n * using browser localStorage. Use this property to provide custom\n * storage backends or testing implementations.\n *\n * @defaultValue new TokenStorage()\n *\n * @example\n * ```typescript\n * // Custom encrypted storage\n * const encryptedStorage = new EncryptedTokenStorage();\n * const config: CoSecConfig = {\n * appId: 'my-app',\n * tokenStorage: encryptedStorage\n * };\n *\n * // Memory storage for testing\n * const memoryStorage = new InMemoryTokenStorage();\n * const config: CoSecConfig = {\n * appId: 'my-app',\n * tokenStorage: memoryStorage\n * };\n * ```\n */\n readonly tokenStorage?: TokenStorage;\n\n /**\n * Custom implementation for storing and retrieving device identifiers.\n *\n * If not provided, a default DeviceIdStorage instance will be created\n * using browser localStorage with cross-tab synchronization. Use this\n * property for custom device identification strategies.\n *\n * @defaultValue new DeviceIdStorage()\n *\n * @example\n * ```typescript\n * // Custom device ID with custom key\n * const customDeviceStorage = new DeviceIdStorage({\n * key: 'my-app-device-id',\n * storage: sessionStorage\n * });\n * const config: CoSecConfig = {\n * appId: 'my-app',\n * deviceIdStorage: customDeviceStorage\n * };\n * ```\n */\n readonly deviceIdStorage?: DeviceIdStorage;\n\n /**\n * Token refresh handler for automatic JWT token renewal.\n *\n * When provided, enables full JWT authentication including:\n * - Automatic Bearer token injection in requests\n * - Token refresh on 401 responses\n * - Persistent token storage\n *\n * When not provided, no authentication interceptors are added.\n *\n * @defaultValue undefined (no authentication)\n *\n * @example\n * ```typescript\n * // Basic refresh implementation\n * const myRefresher: TokenRefresher = {\n * refresh: async (token) => {\n * const response = await fetch('/api/auth/refresh', {\n * method: 'POST',\n * headers: { 'Content-Type': 'application/json' },\n * body: JSON.stringify({ refreshToken: token.refreshToken })\n * });\n * if (!response.ok) throw new Error('Refresh failed');\n * return response.json();\n * }\n * };\n *\n * const config: CoSecConfig = {\n * appId: 'my-app',\n * tokenRefresher: myRefresher\n * };\n * ```\n */\n readonly tokenRefresher?: TokenRefresher;\n\n /**\n * Provider for resolving space identifiers from requests.\n *\n * Used for multi-tenant applications to scope requests to specific\n * spaces within a tenant. When provided, the CoSec-Space-Id header\n * will be added to requests for space-scoped resources.\n *\n * @defaultValue NoneSpaceIdProvider (no space identification)\n *\n * @example\n * ```typescript\n * // Multi-tenant space provider\n * const spaceProvider: SpaceIdProvider = {\n * resolveSpaceId: (exchange) => {\n * return exchange.request.headers['X-Current-Space'] || null;\n * }\n * };\n *\n * const config: CoSecConfig = {\n * appId: 'my-saas-app',\n * tokenRefresher: myRefresher,\n * spaceIdProvider: spaceProvider\n * };\n * ```\n */\n readonly spaceIdProvider?: SpaceIdProvider;\n\n /**\n * Handler invoked when an HTTP 401 Unauthorized response is received.\n *\n * This callback is triggered when the server returns a 401 status code,\n * typically indicating expired or invalid authentication. Use this to\n * implement custom error handling such as redirecting to login.\n *\n * @param exchange - The fetch exchange containing the failed request\n * @returns Promise<void> or void\n *\n * @defaultValue undefined (401 errors not intercepted)\n *\n * @example\n * ```typescript\n * // Redirect to login\n * const config: CoSecConfig = {\n * appId: 'my-app',\n * tokenRefresher: myRefresher,\n * onUnauthorized: async (exchange) => {\n * await authService.logout();\n * window.location.href = '/login?reason=session_expired';\n * }\n * };\n *\n * // Silent token refresh\n * const config: CoSecConfig = {\n * appId: 'my-app',\n * tokenRefresher: myRefresher,\n * onUnauthorized: async (exchange) => {\n * // Force logout without redirect\n * authService.clearSession();\n * dispatch(logoutAction());\n * }\n * };\n * ```\n */\n readonly onUnauthorized?: (exchange: FetchExchange) => Promise<void> | void;\n\n /**\n * Handler invoked when an HTTP 403 Forbidden response is received.\n *\n * This callback is triggered when the server returns a 403 status code,\n * indicating the authenticated user lacks permission for the requested\n * resource. Use this to implement custom access denial handling.\n *\n * @param exchange - The fetch exchange containing the failed request\n * @returns Promise<void>\n *\n * @defaultValue undefined (403 errors not intercepted)\n *\n * @example\n * ```typescript\n * // Show permission error\n * const config: CoSecConfig = {\n * appId: 'my-app',\n * tokenRefresher: myRefresher,\n * onForbidden: async (exchange) => {\n * notification.error({\n * message: 'Access Denied',\n * description: 'You do not have permission to access this resource.'\n * });\n * }\n * };\n *\n * // Redirect to access denied page\n * const config: CoSecConfig = {\n * appId: 'my-app',\n * onForbidden: async (exchange) => {\n * router.push('/access-denied');\n * }\n * };\n * ```\n */\n readonly onForbidden?: (exchange: FetchExchange) => Promise<void>;\n}\n\n/**\n * Configurer class that applies CoSec security interceptors to a Fetcher instance.\n *\n * This class provides a simplified, declarative way to configure all CoSec\n * authentication and security features. It implements FetcherConfigurer and\n * handles the conditional creation and registration of interceptors based\n * on the provided configuration.\n *\n * @remarks\n * **Architecture:**\n * The configurer acts as a composition root for all CoSec interceptors,\n * managing their lifecycle and ensuring proper ordering in the interceptor\n * chain. This approach provides a clean separation between configuration\n * and implementation.\n *\n * **Interceptor Ordering:**\n * Interceptors are registered in a specific order to ensure correct\n * header injection and error handling:\n * 1. CoSecRequestInterceptor (request) - Adds security headers\n * 2. ResourceAttributionRequestInterceptor (request) - Adds tenant attribution\n * 3. AuthorizationRequestInterceptor (request) - Adds Bearer token [conditional]\n * 4. AuthorizationResponseInterceptor (response) - Handles 401 refresh [conditional]\n * 5. UnauthorizedErrorInterceptor (error) - Handles 401 errors [conditional]\n * 6. ForbiddenErrorInterceptor (error) - Handles 403 errors [conditional]\n *\n * **Conditional Features:**\n * - Authentication (tokenRefresher): Enables JWT Bearer token injection and refresh\n * - Space Support (spaceIdProvider): Enables multi-tenant space scoping\n * - Error Handlers (onUnauthorized/onForbidden): Enables custom error handling\n *\n * @example\n * ```typescript\n * import { Fetcher } from '@ahoo-wang/fetcher';\n * import { CoSecConfigurer } from '@ahoo-wang/fetcher-cosec';\n *\n * // Create and configure\n * const configurer = new CoSecConfigurer({\n * appId: 'my-saas-app',\n * tokenRefresher: {\n * refresh: async (token) => {\n * const res = await fetch('/api/refresh', {\n * method: 'POST',\n * body: JSON.stringify({ refreshToken: token.refreshToken })\n * });\n * return res.json();\n * }\n * },\n * onUnauthorized: () => window.location.href = '/login',\n * onForbidden: () => showAccessDenied()\n * });\n *\n * // Apply to fetcher\n * const fetcher = new Fetcher({ baseUrl: 'https://api.example.com' });\n * configurer.applyTo(fetcher);\n *\n * // All requests now have CoSec headers and JWT auth\n * const users = await fetcher.get('/api/users');\n * ```\n *\n * @example\n * ```typescript\n * // Minimal setup - headers only, no authentication\n * const fetcher = new Fetcher();\n * new CoSecConfigurer({\n * appId: 'my-tracking-app'\n * }).applyTo(fetcher);\n *\n * // Requests will include:\n * // - CoSec-App-Id: my-tracking-app\n * // - CoSec-Device-Id: <generated>\n * // - CoSec-Request-Id: <unique-per-request>\n * ```\n */\nexport class CoSecConfigurer implements FetcherConfigurer {\n /**\n * Token storage instance for JWT token persistence.\n *\n * Created from config.tokenStorage or instantiated with default TokenStorage.\n * Used by authorization interceptors to retrieve and store JWT tokens.\n */\n readonly tokenStorage: TokenStorage;\n\n /**\n * Device ID storage instance for device identifier management.\n *\n * Created from config.deviceIdStorage or instantiated with default DeviceIdStorage.\n * Provides persistent device identification with cross-tab synchronization.\n */\n readonly deviceIdStorage: DeviceIdStorage;\n\n /**\n * JWT token manager for authentication operations.\n *\n * Only created when tokenRefresher is provided in the config.\n * When undefined, no authentication interceptors are registered.\n */\n readonly tokenManager?: JwtTokenManager;\n\n /**\n * Space identifier provider for multi-tenant support.\n *\n * Created from config.spaceIdProvider or defaulted to NoneSpaceIdProvider.\n * Resolves space IDs from requests for tenant-scoped resource access.\n */\n readonly spaceIdProvider?: SpaceIdProvider;\n\n /**\n * Creates a new CoSecConfigurer instance with the provided configuration.\n *\n * The constructor performs dependency initialization:\n * - Creates or wraps tokenStorage and deviceIdStorage with defaults\n * - Initializes spaceIdProvider (defaults to NoneSpaceIdProvider)\n * - Conditionally creates tokenManager only if tokenRefresher is provided\n *\n * @param config - CoSec configuration object containing all settings\n *\n * @throws Error if appId is not provided\n * @throws Error if provided dependencies are invalid\n *\n * @example\n * ```typescript\n * // Full configuration\n * const configurer = new CoSecConfigurer({\n * appId: 'my-app',\n * tokenStorage: customTokenStorage,\n * deviceIdStorage: customDeviceStorage,\n * tokenRefresher: myRefresher,\n * spaceIdProvider: mySpaceProvider,\n * onUnauthorized: handle401,\n * onForbidden: handle403\n * });\n *\n * // Minimal configuration\n * const configurer = new CoSecConfigurer({\n * appId: 'my-app'\n * });\n *\n * // Custom storage with default refresh\n * const configurer = new CoSecConfigurer({\n * appId: 'my-app',\n * tokenStorage: encryptedStorage,\n * deviceIdStorage: customDeviceStorage\n * });\n * ```\n */\n constructor(public readonly config: CoSecConfig) {\n // Initialize storage instances with provided or default implementations\n this.tokenStorage = config.tokenStorage ?? new TokenStorage();\n this.deviceIdStorage = config.deviceIdStorage ?? new DeviceIdStorage();\n this.spaceIdProvider = config.spaceIdProvider ?? NoneSpaceIdProvider;\n\n // Conditionally create token manager for authentication\n if (config.tokenRefresher) {\n this.tokenManager = new JwtTokenManager(\n this.tokenStorage,\n config.tokenRefresher,\n );\n }\n }\n\n /**\n * Applies CoSec interceptors to the specified Fetcher instance.\n *\n * This method registers all configured interceptors with the Fetcher's\n * interceptor chain. The registration is conditional based on the config:\n *\n * **Always Registered (Headers & Attribution):**\n * - CoSecRequestInterceptor: Injects security headers\n * - ResourceAttributionRequestInterceptor: Adds tenant attribution parameters\n *\n * **Conditional Registration (Authentication):**\n * - AuthorizationRequestInterceptor: Adds Bearer token [requires tokenRefresher]\n * - AuthorizationResponseInterceptor: Handles 401 refresh [requires tokenRefresher]\n *\n * **Conditional Registration (Error Handling):**\n * - UnauthorizedErrorInterceptor: Custom 401 handling [requires onUnauthorized]\n * - ForbiddenErrorInterceptor: Custom 403 handling [requires onForbidden]\n *\n * @param fetcher - The Fetcher instance to configure with CoSec interceptors\n *\n * @throws Error if fetcher is null or undefined\n *\n * @example\n * ```typescript\n * const fetcher = new Fetcher({ baseUrl: 'https://api.example.com' });\n *\n * const configurer = new CoSecConfigurer({\n * appId: 'my-app',\n * tokenRefresher: myRefresher,\n * onUnauthorized: () => redirectToLogin(),\n * onForbidden: () => showError()\n * });\n *\n * configurer.applyTo(fetcher);\n *\n * // Now fetcher has all CoSec interceptors configured\n * const data = await fetcher.get('/api/data');\n * ```\n *\n * @example\n * ```typescript\n * // Applying to multiple fetchers\n * const apiFetcher = new Fetcher({ baseUrl: 'https://api.example.com' });\n * const adminFetcher = new Fetcher({ baseUrl: 'https://admin.example.com' });\n *\n * const configurer = new CoSecConfigurer({\n * appId: 'my-app',\n * tokenRefresher: myRefresher,\n * onForbidden: showAccessDenied\n * });\n *\n * // Apply same configuration to multiple fetchers\n * configurer.applyTo(apiFetcher);\n * configurer.applyTo(adminFetcher);\n * ```\n */\n applyTo(fetcher: Fetcher): void {\n // Register CoSec request interceptor for security headers\n fetcher.interceptors.request.use(\n new CoSecRequestInterceptor({\n appId: this.config.appId,\n deviceIdStorage: this.deviceIdStorage,\n spaceIdProvider: this.spaceIdProvider,\n }),\n );\n\n // Register resource attribution for tenant/owner path parameters\n fetcher.interceptors.request.use(\n new ResourceAttributionRequestInterceptor({\n tokenStorage: this.tokenStorage,\n }),\n );\n\n // Conditionally register authentication interceptors\n if (this.tokenManager) {\n // Authorization header injection\n fetcher.interceptors.request.use(\n new AuthorizationRequestInterceptor({\n tokenManager: this.tokenManager,\n }),\n );\n\n // 401 response handling and token refresh\n fetcher.interceptors.response.use(\n new AuthorizationResponseInterceptor({\n tokenManager: this.tokenManager,\n }),\n );\n }\n\n // Conditionally register error handlers\n if (this.config.onUnauthorized) {\n fetcher.interceptors.error.use(\n new UnauthorizedErrorInterceptor({\n onUnauthorized: this.config.onUnauthorized,\n }),\n );\n }\n\n if (this.config.onForbidden) {\n fetcher.interceptors.error.use(\n new ForbiddenErrorInterceptor({\n onForbidden: this.config.onForbidden,\n }),\n );\n }\n }\n}\n","/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { Fetcher} from '@ahoo-wang/fetcher';\nimport { ResultExtractors } from '@ahoo-wang/fetcher';\nimport { IGNORE_REFRESH_TOKEN_ATTRIBUTE_KEY } from './cosecRequestInterceptor';\n\n/**\n * Interface representing an access token used for authentication.\n *\n * This interface defines the structure for access tokens, which are typically\n * short-lived tokens used to authenticate API requests.\n *\n * @interface AccessToken\n * @property {string} accessToken - The access token string used for authentication.\n */\nexport interface AccessToken {\n accessToken: string;\n}\n\n/**\n * Interface representing a refresh token used to obtain new access tokens.\n *\n * Refresh tokens are long-lived tokens that can be used to request new access\n * tokens without requiring the user to re-authenticate.\n *\n * @interface RefreshToken\n * @property {string} refreshToken - The refresh token string used to obtain new access tokens.\n */\nexport interface RefreshToken {\n refreshToken: string;\n}\n\n/**\n * Composite token interface that contains both access and refresh tokens.\n *\n * This interface combines AccessToken and RefreshToken, ensuring that both\n * tokens are always handled together. In authentication systems, access and\n * refresh tokens are typically issued and refreshed as a pair.\n *\n * @interface CompositeToken\n * @extends AccessToken\n * @extends RefreshToken\n * @property {string} accessToken - The access token string used for authentication.\n * @property {string} refreshToken - The refresh token string used to obtain new access tokens.\n */\nexport interface CompositeToken extends AccessToken, RefreshToken {}\n\n/**\n * Interface for token refreshers.\n *\n * This interface defines the contract for classes responsible for refreshing\n * authentication tokens. Implementations should handle the logic of exchanging\n * an expired or soon-to-expire token for a new one.\n *\n * **CRITICAL**: When implementing the `refresh` method, always include the\n * `attributes: new Map([[IGNORE_REFRESH_TOKEN_ATTRIBUTE_KEY, true]])` in the\n * request options to prevent infinite loops. Without this attribute, the request\n * interceptor may attempt to refresh the token recursively, causing a deadlock.\n *\n * @interface TokenRefresher\n */\nexport interface TokenRefresher {\n /**\n * Refreshes the given composite token and returns a new one.\n *\n * This method should send the provided token to the appropriate endpoint\n * and return the refreshed token pair. **IMPORTANT**: The implementation must\n * include `attributes: new Map([[IGNORE_REFRESH_TOKEN_ATTRIBUTE_KEY, true]])`\n * in the request options to prevent recursive refresh attempts that could\n * cause infinite loops.\n *\n * @param {CompositeToken} token - The composite token to refresh, containing both access and refresh tokens.\n * @returns {Promise<CompositeToken>} A promise that resolves to a new CompositeToken with updated access and refresh tokens.\n * @throws {Error} Throws an error if the token refresh fails due to network issues, invalid tokens, or server errors.\n *\n * @example\n * ```typescript\n * const refresher: TokenRefresher = new CoSecTokenRefresher({ fetcher, endpoint: '/auth/refresh' });\n * const newToken = await refresher.refresh(currentToken);\n * console.log('New access token:', newToken.accessToken);\n * ```\n *\n * @warning **Critical**: Failing to set the `IGNORE_REFRESH_TOKEN_ATTRIBUTE_KEY`\n * attribute will result in infinite loops during token refresh operations.\n */\n refresh(token: CompositeToken): Promise<CompositeToken>;\n}\n\n/**\n * Configuration options for the CoSecTokenRefresher class.\n *\n * This interface defines the required options to initialize a CoSecTokenRefresher\n * instance, including the HTTP client and the refresh endpoint URL.\n *\n * @interface CoSecTokenRefresherOptions\n * @property {Fetcher} fetcher - The HTTP client instance used to make requests to the refresh endpoint.\n * @property {string} endpoint - The URL endpoint where token refresh requests are sent.\n */\nexport interface CoSecTokenRefresherOptions {\n fetcher: Fetcher;\n endpoint: string;\n}\n\n/**\n * Implementation of TokenRefresher for refreshing composite tokens using CoSec authentication.\n *\n * This class provides a concrete implementation of the TokenRefresher interface,\n * using the provided Fetcher instance to send POST requests to a specified endpoint\n * for token refresh operations. It is designed to work with CoSec (Corporate Security)\n * authentication systems.\n *\n * @class CoSecTokenRefresher\n * @implements TokenRefresher\n *\n * @example\n * ```typescript\n * import { Fetcher } from '@ahoo-wang/fetcher';\n * import { CoSecTokenRefresher } from './tokenRefresher';\n *\n * const fetcher = new Fetcher();\n * const refresher = new CoSecTokenRefresher({\n * fetcher,\n * endpoint: 'https://api.example.com/auth/refresh'\n * });\n *\n * const currentToken = { accessToken: 'old-token', refreshToken: 'refresh-token' };\n * const newToken = await refresher.refresh(currentToken);\n * ```\n */\nexport class CoSecTokenRefresher implements TokenRefresher {\n /**\n * Creates a new instance of CoSecTokenRefresher.\n *\n * @param {CoSecTokenRefresherOptions} options - The configuration options for the token refresher.\n * @param {Fetcher} options.fetcher - The HTTP client instance used for making refresh requests.\n * @param {string} options.endpoint - The URL endpoint for token refresh operations.\n *\n * @example\n * ```typescript\n * const refresher = new CoSecTokenRefresher({\n * fetcher: myFetcherInstance,\n * endpoint: '/api/v1/auth/refresh'\n * });\n * ```\n */\n constructor(public readonly options: CoSecTokenRefresherOptions) {}\n\n /**\n * Refreshes the given composite token by sending a POST request to the configured endpoint.\n *\n * This method sends the current token pair to the refresh endpoint and expects\n * a new CompositeToken in response.\n *\n * **CRITICAL**: The request includes a special\n * attribute `attributes: new Map([[IGNORE_REFRESH_TOKEN_ATTRIBUTE_KEY, true]])`\n * to prevent infinite loops. Without this attribute, the request interceptor\n * may attempt to refresh the token again, causing a recursive loop.\n *\n * @param {CompositeToken} token - The composite token to refresh, containing both access and refresh tokens.\n * @returns {Promise<CompositeToken>} A promise that resolves to a new CompositeToken with refreshed tokens.\n * @throws {Error} Throws an error if the HTTP request fails, the server returns an error status, or the response cannot be parsed as JSON.\n * @throws {NetworkError} Throws a network error if there are connectivity issues.\n * @throws {AuthenticationError} Throws an authentication error if the refresh token is invalid or expired.\n *\n * @example\n * ```typescript\n * const refresher = new CoSecTokenRefresher({ fetcher, endpoint: '/auth/refresh' });\n * const token = { accessToken: 'expired-token', refreshToken: 'valid-refresh-token' };\n *\n * try {\n * const newToken = await refresher.refresh(token);\n * console.log('Refreshed access token:', newToken.accessToken);\n * } catch (error) {\n * console.error('Token refresh failed:', error.message);\n * }\n * ```\n *\n * @warning **Important**: Always include the `IGNORE_REFRESH_TOKEN_ATTRIBUTE_KEY` attribute\n * in refresh requests to avoid infinite loops caused by recursive token refresh attempts.\n */\n refresh(token: CompositeToken): Promise<CompositeToken> {\n // Send a POST request to the configured endpoint with the token as body\n // and extract the response as JSON to return a new CompositeToken\n\n return this.options.fetcher.post<CompositeToken>(\n this.options.endpoint,\n {\n body: token,\n },\n {\n resultExtractor: ResultExtractors.Json,\n attributes: new Map([[IGNORE_REFRESH_TOKEN_ATTRIBUTE_KEY, true]]),\n },\n );\n }\n}\n"],"mappings":"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"}
|