@ahoo-wang/fetcher-cosec 3.3.6 → 3.3.8

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/index.es.js CHANGED
@@ -532,16 +532,53 @@ class he {
532
532
  /**
533
533
  * Creates a new instance of CoSecTokenRefresher.
534
534
  *
535
- * @param options The configuration options for the token refresher including fetcher and endpoint
535
+ * @param {CoSecTokenRefresherOptions} options - The configuration options for the token refresher.
536
+ * @param {Fetcher} options.fetcher - The HTTP client instance used for making refresh requests.
537
+ * @param {string} options.endpoint - The URL endpoint for token refresh operations.
538
+ *
539
+ * @example
540
+ * ```typescript
541
+ * const refresher = new CoSecTokenRefresher({
542
+ * fetcher: myFetcherInstance,
543
+ * endpoint: '/api/v1/auth/refresh'
544
+ * });
545
+ * ```
536
546
  */
537
547
  constructor(e) {
538
548
  this.options = e;
539
549
  }
540
550
  /**
541
- * Refresh the given token and return a new CompositeToken.
551
+ * Refreshes the given composite token by sending a POST request to the configured endpoint.
552
+ *
553
+ * This method sends the current token pair to the refresh endpoint and expects
554
+ * a new CompositeToken in response.
555
+ *
556
+ * **CRITICAL**: The request includes a special
557
+ * attribute `attributes: new Map([[IGNORE_REFRESH_TOKEN_ATTRIBUTE_KEY, true]])`
558
+ * to prevent infinite loops. Without this attribute, the request interceptor
559
+ * may attempt to refresh the token again, causing a recursive loop.
560
+ *
561
+ * @param {CompositeToken} token - The composite token to refresh, containing both access and refresh tokens.
562
+ * @returns {Promise<CompositeToken>} A promise that resolves to a new CompositeToken with refreshed tokens.
563
+ * @throws {Error} Throws an error if the HTTP request fails, the server returns an error status, or the response cannot be parsed as JSON.
564
+ * @throws {NetworkError} Throws a network error if there are connectivity issues.
565
+ * @throws {AuthenticationError} Throws an authentication error if the refresh token is invalid or expired.
566
+ *
567
+ * @example
568
+ * ```typescript
569
+ * const refresher = new CoSecTokenRefresher({ fetcher, endpoint: '/auth/refresh' });
570
+ * const token = { accessToken: 'expired-token', refreshToken: 'valid-refresh-token' };
571
+ *
572
+ * try {
573
+ * const newToken = await refresher.refresh(token);
574
+ * console.log('Refreshed access token:', newToken.accessToken);
575
+ * } catch (error) {
576
+ * console.error('Token refresh failed:', error.message);
577
+ * }
578
+ * ```
542
579
  *
543
- * @param token The token to refresh
544
- * @returns A Promise that resolves to a new CompositeToken
580
+ * @warning **Important**: Always include the `IGNORE_REFRESH_TOKEN_ATTRIBUTE_KEY` attribute
581
+ * in refresh requests to avoid infinite loops caused by recursive token refresh attempts.
545
582
  */
546
583
  refresh(e) {
547
584
  return this.options.fetcher.post(
@@ -1 +1 @@
1
- {"version":3,"file":"index.es.js","sources":["../src/types.ts","../src/idGenerator.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 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 * 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 FetchExchange,\n REQUEST_BODY_INTERCEPTOR_ORDER,\n type RequestInterceptor,\n} from '@ahoo-wang/fetcher';\nimport { AppIdCapable, CoSecHeaders, DeviceIdStorageCapable } from './types';\nimport { idGenerator } from './idGenerator';\n\nexport interface CoSecRequestOptions\n extends AppIdCapable,\n DeviceIdStorageCapable {}\n\n/**\n * The name of the CoSecRequestInterceptor.\n */\nexport const COSEC_REQUEST_INTERCEPTOR_NAME = 'CoSecRequestInterceptor';\n\n/**\n * The order of the CoSecRequestInterceptor.\n * Set to REQUEST_BODY_INTERCEPTOR_ORDER + 1000 to ensure it runs after RequestBodyInterceptor.\n */\nexport const COSEC_REQUEST_INTERCEPTOR_ORDER =\n REQUEST_BODY_INTERCEPTOR_ORDER + 1000;\n\nexport const IGNORE_REFRESH_TOKEN_ATTRIBUTE_KEY = 'Ignore-Refresh-Token';\n\n/**\n * Interceptor that automatically adds CoSec authentication headers to requests.\n *\n * This interceptor adds the following headers to each request:\n * - CoSec-Device-Id: Device identifier (stored in localStorage or generated)\n * - CoSec-App-Id: Application identifier\n * - CoSec-Request-Id: Unique request identifier for each request\n *\n * @remarks\n * This interceptor runs after RequestBodyInterceptor but before FetchInterceptor.\n * The order is set to COSEC_REQUEST_INTERCEPTOR_ORDER to ensure it runs after\n * request body processing but before the actual HTTP request is made. This positioning\n * allows for proper authentication header addition after all request body transformations\n * are complete, ensuring that the final request is properly authenticated before\n * being sent over the network.\n */\nexport class CoSecRequestInterceptor implements RequestInterceptor {\n readonly name = COSEC_REQUEST_INTERCEPTOR_NAME;\n readonly order = COSEC_REQUEST_INTERCEPTOR_ORDER;\n private options: CoSecRequestOptions;\n\n /**\n * Creates a new CoSecRequestInterceptor instance.\n * @param options - The CoSec configuration options including appId, deviceIdStorage, and tokenManager\n */\n constructor(options: CoSecRequestOptions) {\n this.options = options;\n }\n\n /**\n * Intercept requests to add CoSec authentication headers.\n *\n * This method adds the following headers to each request:\n * - CoSec-App-Id: The application identifier from the CoSec options\n * - CoSec-Device-Id: A unique device identifier, either retrieved from storage or generated\n * - CoSec-Request-Id: A unique identifier for this specific request\n *\n * @param exchange - The fetch exchange containing the request to process\n *\n * @remarks\n * This method runs after RequestBodyInterceptor but before FetchInterceptor.\n * It ensures that authentication headers are added to the request after all\n * body processing is complete. The positioning allows for proper authentication\n * header addition after all request body transformations are finished, ensuring\n * that the final request is properly authenticated before being sent over the network.\n * This execution order prevents authentication headers from being overwritten by\n * subsequent request processing interceptors.\n *\n * The method also handles token refreshing when the current token is expired but still refreshable.\n * It will attempt to refresh the token before adding the Authorization header to the request.\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.options.deviceIdStorage.getOrCreate();\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.options.appId;\n requestHeaders[CoSecHeaders.DEVICE_ID] = deviceId;\n requestHeaders[CoSecHeaders.REQUEST_ID] = requestId;\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 { FetchExchange, RequestInterceptor } 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\nexport const AUTHORIZATION_REQUEST_INTERCEPTOR_NAME =\n 'AuthorizationRequestInterceptor';\nexport const AUTHORIZATION_REQUEST_INTERCEPTOR_ORDER =\n COSEC_REQUEST_INTERCEPTOR_ORDER + 1000;\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 * 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 { FetchExchange, RequestInterceptor } 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 Number.MAX_SAFE_INTEGER;\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{\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 * @template Payload The type of the JWT payload\n */\nexport interface IJwtToken<Payload extends JwtPayload>\n extends EarlyPeriodCapable {\n readonly token: string;\n readonly payload: Payload | null;\n\n isExpired: boolean;\n}\n\n/**\n * Class representing a JWT token with typed payload\n * @template Payload The type of the JWT payload\n */\nexport class JwtToken<Payload extends JwtPayload>\n implements IJwtToken<Payload>\n{\n public readonly payload: Payload | null;\n\n /**\n * Creates a new JwtToken instance\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 * @returns true if the token is expired, 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\nexport interface RefreshTokenStatusCapable {\n /**\n * Checks if the access token needs to be refreshed\n * @returns true if the access token is expired, false otherwise\n */\n readonly isRefreshNeeded: boolean;\n /**\n * Checks if the refresh token is still valid and can be used to refresh the access token\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 */\nexport class JwtCompositeToken\n implements EarlyPeriodCapable, RefreshTokenStatusCapable\n{\n public readonly access: JwtToken<CoSecJwtPayload>;\n public readonly refresh: JwtToken<JwtPayload>;\n\n /**\n * Creates a new JwtCompositeToken instance\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 * @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 * @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/**\n * Serializer for JwtCompositeToken that handles conversion to and from JSON strings\n */\nexport class JwtCompositeTokenSerializer\n implements Serializer<string, JwtCompositeToken>, EarlyPeriodCapable\n{\n constructor(public readonly earlyPeriod: number = 0) {}\n\n /**\n * Deserializes a JSON string to a JwtCompositeToken\n * @param value The JSON string representation of a composite token\n * @returns A JwtCompositeToken instance\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 * @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\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 { EarlyPeriodCapable } from './jwts';\nimport { KeyStorage, KeyStorageOptions } from '@ahoo-wang/fetcher-storage';\nimport {\n BroadcastTypedEventBus,\n SerialTypedEventBus,\n} from '@ahoo-wang/fetcher-eventbus';\n\nexport const DEFAULT_COSEC_TOKEN_KEY = 'cosec-token';\n\nexport interface TokenStorageOptions\n extends Partial<Omit<KeyStorageOptions<JwtCompositeToken>, 'serializer'>>,\n Partial<EarlyPeriodCapable> {}\n\n/**\n * Storage class for managing access and refresh tokens.\n */\nexport class TokenStorage\n extends KeyStorage<JwtCompositeToken>\n implements EarlyPeriodCapable\n{\n public readonly earlyPeriod: number;\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 setCompositeToken(compositeToken: CompositeToken) {\n this.set(new JwtCompositeToken(compositeToken, this.earlyPeriod));\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';\n\n/**\n * Simplified configuration interface for CoSec setup.\n * Provides flexible configuration with sensible defaults for optional components.\n */\nexport interface CoSecConfig\n extends AppIdCapable,\n Partial<DeviceIdStorageCapable> {\n /**\n * Application ID to be sent in the CoSec-App-Id header.\n * This is required for identifying your application in the CoSec system.\n */\n appId: string;\n\n /**\n * Custom token storage implementation.\n * If not provided, a default TokenStorage instance will be created.\n * Useful for custom storage backends or testing scenarios.\n */\n tokenStorage?: TokenStorage;\n\n /**\n * Custom device ID storage implementation.\n * If not provided, a default DeviceIdStorage instance will be created.\n * Useful for custom device identification strategies or testing scenarios.\n */\n deviceIdStorage?: DeviceIdStorage;\n\n /**\n * Token refresher implementation for handling expired tokens.\n * If not provided, authentication interceptors will not be added.\n * This enables CoSec configuration without full JWT authentication.\n */\n tokenRefresher?: TokenRefresher;\n\n /**\n * Callback function invoked when an unauthorized (401) response is detected.\n * If not provided, 401 errors will not be intercepted.\n */\n onUnauthorized?: (exchange: FetchExchange) => Promise<void> | void;\n\n /**\n * Callback function invoked when a forbidden (403) response is detected.\n * If not provided, 403 errors will not be intercepted.\n */\n onForbidden?: (exchange: FetchExchange) => Promise<void>;\n}\n\n/**\n * CoSecConfigurer provides a flexible way to configure CoSec interceptors\n * and dependencies with a single configuration object.\n *\n * This class implements FetcherConfigurer and supports both full authentication\n * setups and minimal CoSec header injection. It conditionally creates dependencies\n * based on the provided configuration, allowing for different levels of integration.\n *\n * @implements {FetcherConfigurer}\n *\n * @example\n * Full authentication setup with custom storage:\n * ```typescript\n * const configurer = new CoSecConfigurer({\n * appId: 'my-app-001',\n * tokenStorage: new CustomTokenStorage(),\n * deviceIdStorage: new CustomDeviceStorage(),\n * tokenRefresher: {\n * refresh: async (token: CompositeToken) => {\n * const response = await fetch('/api/auth/refresh', {\n * method: 'POST',\n * body: JSON.stringify({ refreshToken: token.refreshToken }),\n * });\n * const newTokens = await response.json();\n * return {\n * accessToken: newTokens.accessToken,\n * refreshToken: newTokens.refreshToken,\n * };\n * },\n * },\n * onUnauthorized: (exchange) => redirectToLogin(),\n * onForbidden: (exchange) => showPermissionError(),\n * });\n *\n * configurer.applyTo(fetcher);\n * ```\n *\n * @example\n * Minimal setup with only CoSec headers (no authentication):\n * ```typescript\n * const configurer = new CoSecConfigurer({\n * appId: 'my-app-001',\n * // No tokenRefresher provided - authentication interceptors won't be added\n * });\n *\n * configurer.applyTo(fetcher);\n * ```\n */\nexport class CoSecConfigurer implements FetcherConfigurer {\n /**\n * Token storage instance, either provided in config or auto-created.\n */\n readonly tokenStorage: TokenStorage;\n\n /**\n * Device ID storage instance, either provided in config or auto-created.\n */\n readonly deviceIdStorage: DeviceIdStorage;\n\n /**\n * JWT token manager instance, only created if tokenRefresher is provided.\n * When undefined, authentication interceptors will not be added.\n */\n readonly tokenManager?: JwtTokenManager;\n\n /**\n * Creates a new CoSecConfigurer instance with the provided configuration.\n *\n * This constructor conditionally creates dependencies based on the configuration:\n * - TokenStorage and DeviceIdStorage are always created (using defaults if not provided)\n * - JwtTokenManager is only created if tokenRefresher is provided\n *\n * @param config - CoSec configuration object\n *\n * @example\n * ```typescript\n * // Full setup with all dependencies\n * const configurer = new CoSecConfigurer({\n * appId: 'my-app',\n * tokenRefresher: myTokenRefresher,\n * });\n *\n * // Minimal setup with custom storage\n * const configurer = new CoSecConfigurer({\n * appId: 'my-app',\n * tokenStorage: customStorage,\n * deviceIdStorage: customDeviceStorage,\n * });\n * ```\n */\n constructor(public readonly config: CoSecConfig) {\n // Create storage instances with fallbacks to defaults\n this.tokenStorage = config.tokenStorage ?? new TokenStorage();\n this.deviceIdStorage = config.deviceIdStorage ?? new DeviceIdStorage();\n\n // Create token manager only if token refresher is provided\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 provided Fetcher instance.\n *\n * This method conditionally configures interceptors based on the provided configuration:\n *\n * Always added:\n * 1. CoSecRequestInterceptor - Adds CoSec headers (appId, deviceId, requestId)\n * 2. ResourceAttributionRequestInterceptor - Adds tenant/owner path parameters\n *\n * Only when `tokenRefresher` is provided:\n * 3. AuthorizationRequestInterceptor - Adds Bearer token authentication\n * 4. AuthorizationResponseInterceptor - Handles token refresh on 401 responses\n *\n * Only when corresponding handlers are provided:\n * 5. UnauthorizedErrorInterceptor - Handles 401 unauthorized errors\n * 6. ForbiddenErrorInterceptor - Handles 403 forbidden errors\n *\n * @param fetcher - The Fetcher instance to configure\n *\n * @example\n * ```typescript\n * const fetcher = new Fetcher({ baseURL: '/api' });\n * const configurer = new CoSecConfigurer({\n * appId: 'my-app',\n * tokenRefresher: myTokenRefresher,\n * onUnauthorized: handle401,\n * onForbidden: handle403,\n * });\n *\n * configurer.applyTo(fetcher);\n * // Now fetcher has all CoSec interceptors configured\n * ```\n */\n applyTo(fetcher: Fetcher): void {\n fetcher.interceptors.request.use(\n new CoSecRequestInterceptor({\n appId: this.config.appId,\n deviceIdStorage: this.deviceIdStorage,\n }),\n );\n\n fetcher.interceptors.request.use(\n new ResourceAttributionRequestInterceptor({\n tokenStorage: this.tokenStorage,\n }),\n );\n if (this.tokenManager) {\n fetcher.interceptors.request.use(\n new AuthorizationRequestInterceptor({\n tokenManager: this.tokenManager,\n }),\n );\n\n fetcher.interceptors.response.use(\n new AuthorizationResponseInterceptor({\n tokenManager: this.tokenManager,\n }),\n );\n }\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 for access tokens.\n */\nexport interface AccessToken {\n accessToken: string;\n}\n\n/**\n * Interface for refresh tokens.\n */\nexport interface RefreshToken {\n refreshToken: string;\n}\n\n/**\n * Composite token interface that contains both access and refresh tokens.\n *\n * accessToken and refreshToken always appear in pairs, no need to split them.\n */\nexport interface CompositeToken extends AccessToken, RefreshToken {}\n\n/**\n * Interface for token refreshers.\n *\n * Provides a method to refresh tokens.\n */\nexport interface TokenRefresher {\n /**\n * Refresh the given token and return a new CompositeToken.\n *\n * @param token The token to refresh\n * @returns A Promise that resolves to a new CompositeToken\n */\n refresh(token: CompositeToken): Promise<CompositeToken>;\n}\n\nexport interface CoSecTokenRefresherOptions {\n fetcher: Fetcher;\n endpoint: string;\n}\n\n/**\n * CoSecTokenRefresher is a class that implements the TokenRefresher interface\n * for refreshing composite tokens through a configured endpoint.\n */\nexport class CoSecTokenRefresher implements TokenRefresher {\n /**\n * Creates a new instance of CoSecTokenRefresher.\n *\n * @param options The configuration options for the token refresher including fetcher and endpoint\n */\n constructor(public readonly options: CoSecTokenRefresherOptions) {}\n\n /**\n * Refresh the given token and return a new CompositeToken.\n *\n * @param token The token to refresh\n * @returns A Promise that resolves to a new CompositeToken\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","COSEC_REQUEST_INTERCEPTOR_NAME","COSEC_REQUEST_INTERCEPTOR_ORDER","REQUEST_BODY_INTERCEPTOR_ORDER","IGNORE_REFRESH_TOKEN_ATTRIBUTE_KEY","CoSecRequestInterceptor","options","exchange","requestId","deviceId","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","KeyStorage","key","eventBus","BroadcastTypedEventBus","SerialTypedEventBus","reset","typedIdentitySerializer","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","ResourceAttributionRequestInterceptor","tenantId","ownerId","principal","extractedPathParams","tenantIdPathKey","requestPathParams","ownerIdPathKey","parseJwtPayload","parts","base64","paddedBase64","jsonPayload","c","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":";;;;AAmBO,MAAMA,IAAN,MAAMA,EAAa;AAK1B;AAJEA,EAAgB,YAAY,mBAC5BA,EAAgB,SAAS,gBACzBA,EAAgB,gBAAgB,iBAChCA,EAAgB,aAAa;AAJxB,IAAMC,IAAND;AAOA,MAAME,IAAN,MAAMA,EAAc;AAG3B;AAFEA,EAAgB,eAAe,KAC/BA,EAAgB,YAAY;AAFvB,IAAMC,IAAND;AAuCA,MAAME,KAAmB;AAAA,EAC9B,OAAO,EAAE,YAAY,IAAM,QAAQ,QAAA;AAAA,EACnC,eAAe,EAAE,YAAY,IAAO,QAAQ,gBAAA;AAAA,EAC5C,eAAe,EAAE,YAAY,IAAO,QAAQ,gBAAA;AAAA,EAC5C,eAAe,EAAE,YAAY,IAAO,QAAQ,gBAAA;AAAA,EAC5C,mBAAmB,EAAE,YAAY,IAAO,QAAQ,oBAAA;AAClD;AChDO,MAAMC,EAAuC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMlD,aAAqB;AACnB,WAAOC,EAAA;AAAA,EACT;AACF;AAEO,MAAMC,IAAc,IAAIF,EAAA,GCNlBG,IAAiC,2BAMjCC,IACXC,IAAiC,KAEtBC,IAAqC;AAkB3C,MAAMC,EAAsD;AAAA;AAAA;AAAA;AAAA;AAAA,EASjE,YAAYC,GAA8B;AAR1C,SAAS,OAAOL,GAChB,KAAS,QAAQC,GAQf,KAAK,UAAUI;AAAA,EACjB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAwBA,MAAM,UAAUC,GAAyB;AAEvC,UAAMC,IAAYR,EAAY,WAAA,GAGxBS,IAAW,KAAK,QAAQ,gBAAgB,YAAA,GAGxCC,IAAiBH,EAAS,qBAAA;AAGhC,IAAAG,EAAehB,EAAa,MAAM,IAAI,KAAK,QAAQ,OACnDgB,EAAehB,EAAa,SAAS,IAAIe,GACzCC,EAAehB,EAAa,UAAU,IAAIc;AAAA,EAC5C;AACF;ACjFO,MAAMG,IACX,mCACWC,IACXV,IAAkC;AAY7B,MAAMW,EAA8D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASzE,YAA6BP,GAA0C;AAA1C,SAAA,UAAAA,GAR7B,KAAS,OAAOK,GAChB,KAAS,QAAQC;AAAA,EAOuD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaxE,MAAM,UAAUL,GAAwC;AAEtD,QAAIO,IAAe,KAAK,QAAQ,aAAa;AAE7C,UAAMJ,IAAiBH,EAAS,qBAAA;AAGhC,IAAI,CAACO,KAAgBJ,EAAehB,EAAa,aAAa,MAM5D,CAACa,EAAS,WAAW,IAAIH,CAAkC,KAC3DU,EAAa,mBACbA,EAAa,iBAEb,MAAM,KAAK,QAAQ,aAAa,QAAA,GAIlCA,IAAe,KAAK,QAAQ,aAAa,cAGrCA,MACFJ,EAAehB,EAAa,aAAa,IACvC,UAAUoB,EAAa,OAAO,KAAK;AAAA,EAEzC;AACF;ACtEO,MAAMC,IACX,oCAMWC,IACX,OAAO,mBAAmB;AAYrB,MAAMC,EAAgE;AAAA;AAAA;AAAA;AAAA;AAAA,EAQ3E,YAAoBX,GAA0C;AAA1C,SAAA,UAAAA,GAPpB,KAAS,OAAOS,GAChB,KAAS,QAAQC;AAAA,EAM8C;AAAA;AAAA;AAAA;AAAA;AAAA,EAM/D,MAAM,UAAUT,GAAwC;AACtD,UAAMW,IAAWX,EAAS;AAE1B,QAAKW,KAKDA,EAAS,WAAWtB,EAAc,gBAIjC,KAAK,QAAQ,aAAa;AAG/B,UAAI;AACF,cAAM,KAAK,QAAQ,aAAa,QAAA,GAEhC,MAAMW,EAAS,QAAQ,aAAa,SAASA,CAAQ;AAAA,MACvD,SAASY,GAAO;AAEd,mBAAK,QAAQ,aAAa,aAAa,OAAA,GACjCA;AAAA,MACR;AAAA,EACF;AACF;AC3DO,MAAMC,IAA8B;AAUpC,MAAMC,UAAwBC,EAAmB;AAAA,EACtD,YAAY;AAAA,IACE,KAAAC,IAAMH;AAAA,IACN,UAAAI,IAAW,IAAIC,EAAuB;AAAA,MACpC,UAAU,IAAIC,EAAoBN,CAA2B;AAAA,IAAA,CAC9D;AAAA,IACD,GAAGO;AAAA,EAAA,IACuB,IAAI;AAC1C,UAAM,EAAE,KAAAJ,GAAK,UAAAC,GAAU,GAAGG,GAAO,YAAYC,EAAA,GAA2B;AAAA,EAC1E;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,mBAA2B;AACzB,WAAO5B,EAAY,WAAA;AAAA,EACrB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,cAAsB;AAEpB,QAAIS,IAAW,KAAK,IAAA;AACpB,WAAKA,MAEHA,IAAW,KAAK,iBAAA,GAChB,KAAK,IAAIA,CAAQ,IAGZA;AAAA,EACT;AACF;AC9CO,MAAMoB,IAAmC,6BAMnCC,IAAoC;AAsF1C,MAAMC,EAAsD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA+BjE,YAAoBzB,GAA2C;AAA3C,SAAA,UAAAA,GA1BpB,KAAS,OAAOuB,GAOhB,KAAS,QAAQC;AAAA,EAmB+C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAsChE,MAAM,UAAUvB,GAAwC;AAEtD,IAAIA,EAAS,UAAU,WAAWX,EAAc,aAG9C,MAAM,KAAK,QAAQ,YAAYW,CAAQ;AAAA,EAE3C;AACF;AC3KO,MAAMyB,UAA0BC,EAAa;AAAA,EAClD,YACkBC,GAChBC,GACA;AACA,UAAM,yBAAyBA,CAAK,GAHpB,KAAA,QAAAD,GAIhB,KAAK,OAAO,qBACZ,OAAO,eAAe,MAAMF,EAAkB,SAAS;AAAA,EACzD;AACF;AAKO,MAAMI,EAAqD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQhE,YACkBC,GACAC,GAChB;AAFgB,SAAA,eAAAD,GACA,KAAA,iBAAAC;AAAA,EACf;AAAA;AAAA;AAAA;AAAA;AAAA,EAMH,IAAI,eAAyC;AAC3C,WAAO,KAAK,aAAa,IAAA;AAAA,EAC3B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,UAAyB;AAC7B,UAAMC,IAAW,KAAK;AACtB,QAAI,CAACA;AACH,YAAM,IAAI,MAAM,gBAAgB;AAElC,WAAI,KAAK,oBACA,KAAK,qBAGd,KAAK,oBAAoB,KAAK,eAC3B,QAAQA,EAAS,KAAK,EACtB,KAAK,CAAAC,MAAY;AAChB,WAAK,aAAa,kBAAkBA,CAAQ;AAAA,IAC9C,CAAC,EACA,MAAM,CAAArB,MAAS;AACd,iBAAK,aAAa,OAAA,GACZ,IAAIa,EAAkBO,GAAUpB,CAAK;AAAA,IAC7C,CAAC,EACA,QAAQ,MAAM;AACb,WAAK,oBAAoB;AAAA,IAC3B,CAAC,GAEI,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,IAAI,kBAA2B;AAC7B,WAAK,KAAK,eAGH,KAAK,aAAa,kBAFhB;AAAA,EAGX;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,IAAI,gBAAyB;AAC3B,WAAK,KAAK,eAGH,KAAK,aAAa,gBAFhB;AAAA,EAGX;AACF;ACxFA,MAAMsB,IAAqB,YACrBC,IAAoB,WAuBbC,IACX,yCAIWC,IACX,OAAO;AAOF,MAAMC,EAEb;AAAA;AAAA;AAAA;AAAA;AAAA,EAWE,YAAY;AAAA,IACV,UAAAC,IAAWL;AAAA,IACX,SAAAM,IAAUL;AAAA,IACV,cAAAL;AAAA,EAAA,GAC6B;AAd/B,SAAS,OAAOM,GAChB,KAAS,QAAQC,GAcf,KAAK,kBAAkBE,GACvB,KAAK,iBAAiBC,GACtB,KAAK,eAAeV;AAAA,EACtB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,UAAU9B,GAA+B;AACvC,UAAMO,IAAe,KAAK,aAAa,IAAA;AACvC,QAAI,CAACA;AACH;AAEF,UAAMkC,IAAYlC,EAAa,OAAO;AAItC,QAHI,CAACkC,KAGD,CAACA,EAAU,YAAY,CAACA,EAAU;AACpC;AAIF,UAAMC,IACJ1C,EAAS,QAAQ,WAAW,oBAAoB;AAAA,MAC9CA,EAAS,QAAQ;AAAA,IAAA,GAEf2C,IAAkB,KAAK,iBACvBC,IAAoB5C,EAAS,uBAAA,EAAyB,MACtDuC,IAAWE,EAAU;AAG3B,IACEF,KACAG,EAAoB,SAASC,CAAe,KAC5C,CAACC,EAAkBD,CAAe,MAElCC,EAAkBD,CAAe,IAAIJ;AAGvC,UAAMM,IAAiB,KAAK,gBACtBL,IAAUC,EAAU;AAG1B,IACED,KACAE,EAAoB,SAASG,CAAc,KAC3C,CAACD,EAAkBC,CAAc,MAEjCD,EAAkBC,CAAc,IAAIL;AAAA,EAExC;AACF;AClCO,SAASM,EAAsCnB,GAAyB;AAC7E,MAAI;AACF,QAAI,OAAOA,KAAU;AACnB,aAAO;AAET,UAAMoB,IAAQpB,EAAM,MAAM,GAAG;AAC7B,QAAIoB,EAAM,WAAW;AACnB,aAAO;AAIT,UAAMC,IADYD,EAAM,CAAC,EACA,QAAQ,MAAM,GAAG,EAAE,QAAQ,MAAM,GAAG,GAGvDE,IAAeD,EAAO;AAAA,MAC1BA,EAAO,UAAW,IAAKA,EAAO,SAAS,KAAM;AAAA,MAC7C;AAAA,IAAA,GAGIE,IAAc;AAAA,MAClB,KAAKD,CAAY,EACd,MAAM,EAAE,EACR,IAAI,SAAUE,GAAG;AAChB,eAAO,OAAO,OAAOA,EAAE,WAAW,CAAC,EAAE,SAAS,EAAE,GAAG,MAAM,EAAE;AAAA,MAC7D,CAAC,EACA,KAAK,EAAE;AAAA,IAAA;AAEZ,WAAO,KAAK,MAAMD,CAAW;AAAA,EAC/B,SAAStC,GAAO;AAEd,mBAAQ,MAAM,6BAA6BA,CAAK,GACzC;AAAA,EACT;AACF;AAyBO,SAASwC,EACdzB,GACA0B,IAAsB,GACb;AACT,QAAMC,IAAU,OAAO3B,KAAU,WAAWmB,EAAgBnB,CAAK,IAAIA;AACrE,MAAI,CAAC2B;AACH,WAAO;AAGT,QAAMC,IAAQD,EAAQ;AACtB,SAAKC,IAIO,KAAK,IAAA,IAAQ,MACZA,IAAQF,IAJZ;AAKX;AC7HO,MAAMG,EAEb;AAAA;AAAA;AAAA;AAAA,EAME,YACkB7B,GACA0B,IAAsB,GACtC;AAFgB,SAAA,QAAA1B,GACA,KAAA,cAAA0B,GAEhB,KAAK,UAAUP,EAAyBnB,CAAK;AAAA,EAC/C;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,IAAI,YAAqB;AACvB,WAAK,KAAK,UAGHyB,EAAe,KAAK,SAAS,KAAK,WAAW,IAF3C;AAAA,EAGX;AACF;AAkBO,MAAMK,EAEb;AAAA;AAAA;AAAA;AAAA,EAOE,YACkB9B,GACA0B,IAAsB,GACtC;AAFgB,SAAA,QAAA1B,GACA,KAAA,cAAA0B,GAEhB,KAAK,SAAS,IAAIG,EAAS7B,EAAM,aAAa0B,CAAW,GACzD,KAAK,UAAU,IAAIG,EAAS7B,EAAM,cAAc0B,CAAW;AAAA,EAC7D;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,IAAI,kBAA2B;AAC7B,WAAO,KAAK,OAAO;AAAA,EACrB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,IAAI,gBAAyB;AAC3B,WAAO,CAAC,KAAK,QAAQ;AAAA,EACvB;AACF;AAKO,MAAMK,EAEb;AAAA,EACE,YAA4BL,IAAsB,GAAG;AAAzB,SAAA,cAAAA;AAAA,EAA0B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOtD,YAAYM,GAAkC;AAC5C,UAAMC,IAAiB,KAAK,MAAMD,CAAK;AACvC,WAAO,IAAIF,EAAkBG,GAAgB,KAAK,WAAW;AAAA,EAC/D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,UAAUD,GAAkC;AAC1C,WAAO,KAAK,UAAUA,EAAM,KAAK;AAAA,EACnC;AACF;AAEO,MAAME,KAA8B,IAAIH,EAAA,GC1HlCI,IAA0B;AAShC,MAAMC,UACHhD,EAEV;AAAA,EAGE,YAAY;AAAA,IACV,KAAAC,IAAM8C;AAAA,IACN,UAAA7C,IAAW,IAAIC,EAAuB;AAAA,MACpC,UAAU,IAAIC,EAAoB2C,CAAuB;AAAA,IAAA,CAC1D;AAAA,IACD,aAAAT,IAAc;AAAA,IACd,GAAGjC;AAAA,EAAA,IACoB,IAAI;AAC3B,UAAM;AAAA,MACJ,KAAAJ;AAAA,MACA,UAAAC;AAAA,MACA,GAAGG;AAAA,MACH,YAAY,IAAIsC,EAA4BL,CAAW;AAAA,IAAA,CACxD,GACD,KAAK,cAAcA;AAAA,EACrB;AAAA,EAEA,kBAAkBO,GAAgC;AAChD,SAAK,IAAI,IAAIH,EAAkBG,GAAgB,KAAK,WAAW,CAAC;AAAA,EAClE;AACF;ACrCO,MAAMI,IACX,gCAMWC,KAAuC;AA6C7C,MAAMC,GAAyD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAgBpE,YAAoBnE,GAA8C;AAA9C,SAAA,UAAAA,GAZpB,KAAS,OAAOiE,GAKhB,KAAS,QAAQC;AAAA,EAOkD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAyBnE,MAAM,UAAUjE,GAAwC;AACtD,KACEA,EAAS,UAAU,WAAWX,EAAc,gBAC5CW,EAAS,iBAAiByB,MAE1B,MAAM,KAAK,QAAQ,eAAezB,CAAQ;AAAA,EAE9C;AACF;ACAO,MAAMmE,GAA6C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA0CxD,YAA4BC,GAAqB;AAArB,SAAA,SAAAA,GAE1B,KAAK,eAAeA,EAAO,gBAAgB,IAAIL,EAAA,GAC/C,KAAK,kBAAkBK,EAAO,mBAAmB,IAAItD,EAAA,GAGjDsD,EAAO,mBACT,KAAK,eAAe,IAAIvC;AAAA,MACtB,KAAK;AAAA,MACLuC,EAAO;AAAA,IAAA;AAAA,EAGb;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAmCA,QAAQC,GAAwB;AAC9B,IAAAA,EAAQ,aAAa,QAAQ;AAAA,MAC3B,IAAIvE,EAAwB;AAAA,QAC1B,OAAO,KAAK,OAAO;AAAA,QACnB,iBAAiB,KAAK;AAAA,MAAA,CACvB;AAAA,IAAA,GAGHuE,EAAQ,aAAa,QAAQ;AAAA,MAC3B,IAAI/B,EAAsC;AAAA,QACxC,cAAc,KAAK;AAAA,MAAA,CACpB;AAAA,IAAA,GAEC,KAAK,iBACP+B,EAAQ,aAAa,QAAQ;AAAA,MAC3B,IAAI/D,EAAgC;AAAA,QAClC,cAAc,KAAK;AAAA,MAAA,CACpB;AAAA,IAAA,GAGH+D,EAAQ,aAAa,SAAS;AAAA,MAC5B,IAAI3D,EAAiC;AAAA,QACnC,cAAc,KAAK;AAAA,MAAA,CACpB;AAAA,IAAA,IAGD,KAAK,OAAO,kBACd2D,EAAQ,aAAa,MAAM;AAAA,MACzB,IAAIH,GAA6B;AAAA,QAC/B,gBAAgB,KAAK,OAAO;AAAA,MAAA,CAC7B;AAAA,IAAA,GAID,KAAK,OAAO,eACdG,EAAQ,aAAa,MAAM;AAAA,MACzB,IAAI7C,EAA0B;AAAA,QAC5B,aAAa,KAAK,OAAO;AAAA,MAAA,CAC1B;AAAA,IAAA;AAAA,EAGP;AACF;AC/LO,MAAM8C,GAA8C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMzD,YAA4BvE,GAAqC;AAArC,SAAA,UAAAA;AAAA,EAAsC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQlE,QAAQ4B,GAAgD;AAItD,WAAO,KAAK,QAAQ,QAAQ;AAAA,MAC1B,KAAK,QAAQ;AAAA,MACb;AAAA,QACE,MAAMA;AAAA,MAAA;AAAA,MAER;AAAA,QACE,iBAAiB4C,EAAiB;AAAA,QAClC,gCAAgB,IAAI,CAAC,CAAC1E,GAAoC,EAAI,CAAC,CAAC;AAAA,MAAA;AAAA,IAClE;AAAA,EAEJ;AACF;"}
1
+ {"version":3,"file":"index.es.js","sources":["../src/types.ts","../src/idGenerator.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 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 * 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 FetchExchange,\n REQUEST_BODY_INTERCEPTOR_ORDER,\n type RequestInterceptor,\n} from '@ahoo-wang/fetcher';\nimport { AppIdCapable, CoSecHeaders, DeviceIdStorageCapable } from './types';\nimport { idGenerator } from './idGenerator';\n\nexport interface CoSecRequestOptions\n extends AppIdCapable,\n DeviceIdStorageCapable {}\n\n/**\n * The name of the CoSecRequestInterceptor.\n */\nexport const COSEC_REQUEST_INTERCEPTOR_NAME = 'CoSecRequestInterceptor';\n\n/**\n * The order of the CoSecRequestInterceptor.\n * Set to REQUEST_BODY_INTERCEPTOR_ORDER + 1000 to ensure it runs after RequestBodyInterceptor.\n */\nexport const COSEC_REQUEST_INTERCEPTOR_ORDER =\n REQUEST_BODY_INTERCEPTOR_ORDER + 1000;\n\nexport const IGNORE_REFRESH_TOKEN_ATTRIBUTE_KEY = 'Ignore-Refresh-Token';\n\n/**\n * Interceptor that automatically adds CoSec authentication headers to requests.\n *\n * This interceptor adds the following headers to each request:\n * - CoSec-Device-Id: Device identifier (stored in localStorage or generated)\n * - CoSec-App-Id: Application identifier\n * - CoSec-Request-Id: Unique request identifier for each request\n *\n * @remarks\n * This interceptor runs after RequestBodyInterceptor but before FetchInterceptor.\n * The order is set to COSEC_REQUEST_INTERCEPTOR_ORDER to ensure it runs after\n * request body processing but before the actual HTTP request is made. This positioning\n * allows for proper authentication header addition after all request body transformations\n * are complete, ensuring that the final request is properly authenticated before\n * being sent over the network.\n */\nexport class CoSecRequestInterceptor implements RequestInterceptor {\n readonly name = COSEC_REQUEST_INTERCEPTOR_NAME;\n readonly order = COSEC_REQUEST_INTERCEPTOR_ORDER;\n private options: CoSecRequestOptions;\n\n /**\n * Creates a new CoSecRequestInterceptor instance.\n * @param options - The CoSec configuration options including appId, deviceIdStorage, and tokenManager\n */\n constructor(options: CoSecRequestOptions) {\n this.options = options;\n }\n\n /**\n * Intercept requests to add CoSec authentication headers.\n *\n * This method adds the following headers to each request:\n * - CoSec-App-Id: The application identifier from the CoSec options\n * - CoSec-Device-Id: A unique device identifier, either retrieved from storage or generated\n * - CoSec-Request-Id: A unique identifier for this specific request\n *\n * @param exchange - The fetch exchange containing the request to process\n *\n * @remarks\n * This method runs after RequestBodyInterceptor but before FetchInterceptor.\n * It ensures that authentication headers are added to the request after all\n * body processing is complete. The positioning allows for proper authentication\n * header addition after all request body transformations are finished, ensuring\n * that the final request is properly authenticated before being sent over the network.\n * This execution order prevents authentication headers from being overwritten by\n * subsequent request processing interceptors.\n *\n * The method also handles token refreshing when the current token is expired but still refreshable.\n * It will attempt to refresh the token before adding the Authorization header to the request.\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.options.deviceIdStorage.getOrCreate();\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.options.appId;\n requestHeaders[CoSecHeaders.DEVICE_ID] = deviceId;\n requestHeaders[CoSecHeaders.REQUEST_ID] = requestId;\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 { FetchExchange, RequestInterceptor } 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\nexport const AUTHORIZATION_REQUEST_INTERCEPTOR_NAME =\n 'AuthorizationRequestInterceptor';\nexport const AUTHORIZATION_REQUEST_INTERCEPTOR_ORDER =\n COSEC_REQUEST_INTERCEPTOR_ORDER + 1000;\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 * 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 { FetchExchange, RequestInterceptor } 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 Number.MAX_SAFE_INTEGER;\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{\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 * @template Payload The type of the JWT payload\n */\nexport interface IJwtToken<Payload extends JwtPayload>\n extends EarlyPeriodCapable {\n readonly token: string;\n readonly payload: Payload | null;\n\n isExpired: boolean;\n}\n\n/**\n * Class representing a JWT token with typed payload\n * @template Payload The type of the JWT payload\n */\nexport class JwtToken<Payload extends JwtPayload>\n implements IJwtToken<Payload>\n{\n public readonly payload: Payload | null;\n\n /**\n * Creates a new JwtToken instance\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 * @returns true if the token is expired, 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\nexport interface RefreshTokenStatusCapable {\n /**\n * Checks if the access token needs to be refreshed\n * @returns true if the access token is expired, false otherwise\n */\n readonly isRefreshNeeded: boolean;\n /**\n * Checks if the refresh token is still valid and can be used to refresh the access token\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 */\nexport class JwtCompositeToken\n implements EarlyPeriodCapable, RefreshTokenStatusCapable\n{\n public readonly access: JwtToken<CoSecJwtPayload>;\n public readonly refresh: JwtToken<JwtPayload>;\n\n /**\n * Creates a new JwtCompositeToken instance\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 * @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 * @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/**\n * Serializer for JwtCompositeToken that handles conversion to and from JSON strings\n */\nexport class JwtCompositeTokenSerializer\n implements Serializer<string, JwtCompositeToken>, EarlyPeriodCapable\n{\n constructor(public readonly earlyPeriod: number = 0) {}\n\n /**\n * Deserializes a JSON string to a JwtCompositeToken\n * @param value The JSON string representation of a composite token\n * @returns A JwtCompositeToken instance\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 * @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\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 { EarlyPeriodCapable } from './jwts';\nimport { KeyStorage, KeyStorageOptions } from '@ahoo-wang/fetcher-storage';\nimport {\n BroadcastTypedEventBus,\n SerialTypedEventBus,\n} from '@ahoo-wang/fetcher-eventbus';\n\nexport const DEFAULT_COSEC_TOKEN_KEY = 'cosec-token';\n\nexport interface TokenStorageOptions\n extends Partial<Omit<KeyStorageOptions<JwtCompositeToken>, 'serializer'>>,\n Partial<EarlyPeriodCapable> {}\n\n/**\n * Storage class for managing access and refresh tokens.\n */\nexport class TokenStorage\n extends KeyStorage<JwtCompositeToken>\n implements EarlyPeriodCapable\n{\n public readonly earlyPeriod: number;\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 setCompositeToken(compositeToken: CompositeToken) {\n this.set(new JwtCompositeToken(compositeToken, this.earlyPeriod));\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';\n\n/**\n * Simplified configuration interface for CoSec setup.\n * Provides flexible configuration with sensible defaults for optional components.\n */\nexport interface CoSecConfig\n extends AppIdCapable,\n Partial<DeviceIdStorageCapable> {\n /**\n * Application ID to be sent in the CoSec-App-Id header.\n * This is required for identifying your application in the CoSec system.\n */\n appId: string;\n\n /**\n * Custom token storage implementation.\n * If not provided, a default TokenStorage instance will be created.\n * Useful for custom storage backends or testing scenarios.\n */\n tokenStorage?: TokenStorage;\n\n /**\n * Custom device ID storage implementation.\n * If not provided, a default DeviceIdStorage instance will be created.\n * Useful for custom device identification strategies or testing scenarios.\n */\n deviceIdStorage?: DeviceIdStorage;\n\n /**\n * Token refresher implementation for handling expired tokens.\n * If not provided, authentication interceptors will not be added.\n * This enables CoSec configuration without full JWT authentication.\n */\n tokenRefresher?: TokenRefresher;\n\n /**\n * Callback function invoked when an unauthorized (401) response is detected.\n * If not provided, 401 errors will not be intercepted.\n */\n onUnauthorized?: (exchange: FetchExchange) => Promise<void> | void;\n\n /**\n * Callback function invoked when a forbidden (403) response is detected.\n * If not provided, 403 errors will not be intercepted.\n */\n onForbidden?: (exchange: FetchExchange) => Promise<void>;\n}\n\n/**\n * CoSecConfigurer provides a flexible way to configure CoSec interceptors\n * and dependencies with a single configuration object.\n *\n * This class implements FetcherConfigurer and supports both full authentication\n * setups and minimal CoSec header injection. It conditionally creates dependencies\n * based on the provided configuration, allowing for different levels of integration.\n *\n * @implements {FetcherConfigurer}\n *\n * @example\n * Full authentication setup with custom storage:\n * ```typescript\n * const configurer = new CoSecConfigurer({\n * appId: 'my-app-001',\n * tokenStorage: new CustomTokenStorage(),\n * deviceIdStorage: new CustomDeviceStorage(),\n * tokenRefresher: {\n * refresh: async (token: CompositeToken) => {\n * const response = await fetch('/api/auth/refresh', {\n * method: 'POST',\n * body: JSON.stringify({ refreshToken: token.refreshToken }),\n * });\n * const newTokens = await response.json();\n * return {\n * accessToken: newTokens.accessToken,\n * refreshToken: newTokens.refreshToken,\n * };\n * },\n * },\n * onUnauthorized: (exchange) => redirectToLogin(),\n * onForbidden: (exchange) => showPermissionError(),\n * });\n *\n * configurer.applyTo(fetcher);\n * ```\n *\n * @example\n * Minimal setup with only CoSec headers (no authentication):\n * ```typescript\n * const configurer = new CoSecConfigurer({\n * appId: 'my-app-001',\n * // No tokenRefresher provided - authentication interceptors won't be added\n * });\n *\n * configurer.applyTo(fetcher);\n * ```\n */\nexport class CoSecConfigurer implements FetcherConfigurer {\n /**\n * Token storage instance, either provided in config or auto-created.\n */\n readonly tokenStorage: TokenStorage;\n\n /**\n * Device ID storage instance, either provided in config or auto-created.\n */\n readonly deviceIdStorage: DeviceIdStorage;\n\n /**\n * JWT token manager instance, only created if tokenRefresher is provided.\n * When undefined, authentication interceptors will not be added.\n */\n readonly tokenManager?: JwtTokenManager;\n\n /**\n * Creates a new CoSecConfigurer instance with the provided configuration.\n *\n * This constructor conditionally creates dependencies based on the configuration:\n * - TokenStorage and DeviceIdStorage are always created (using defaults if not provided)\n * - JwtTokenManager is only created if tokenRefresher is provided\n *\n * @param config - CoSec configuration object\n *\n * @example\n * ```typescript\n * // Full setup with all dependencies\n * const configurer = new CoSecConfigurer({\n * appId: 'my-app',\n * tokenRefresher: myTokenRefresher,\n * });\n *\n * // Minimal setup with custom storage\n * const configurer = new CoSecConfigurer({\n * appId: 'my-app',\n * tokenStorage: customStorage,\n * deviceIdStorage: customDeviceStorage,\n * });\n * ```\n */\n constructor(public readonly config: CoSecConfig) {\n // Create storage instances with fallbacks to defaults\n this.tokenStorage = config.tokenStorage ?? new TokenStorage();\n this.deviceIdStorage = config.deviceIdStorage ?? new DeviceIdStorage();\n\n // Create token manager only if token refresher is provided\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 provided Fetcher instance.\n *\n * This method conditionally configures interceptors based on the provided configuration:\n *\n * Always added:\n * 1. CoSecRequestInterceptor - Adds CoSec headers (appId, deviceId, requestId)\n * 2. ResourceAttributionRequestInterceptor - Adds tenant/owner path parameters\n *\n * Only when `tokenRefresher` is provided:\n * 3. AuthorizationRequestInterceptor - Adds Bearer token authentication\n * 4. AuthorizationResponseInterceptor - Handles token refresh on 401 responses\n *\n * Only when corresponding handlers are provided:\n * 5. UnauthorizedErrorInterceptor - Handles 401 unauthorized errors\n * 6. ForbiddenErrorInterceptor - Handles 403 forbidden errors\n *\n * @param fetcher - The Fetcher instance to configure\n *\n * @example\n * ```typescript\n * const fetcher = new Fetcher({ baseURL: '/api' });\n * const configurer = new CoSecConfigurer({\n * appId: 'my-app',\n * tokenRefresher: myTokenRefresher,\n * onUnauthorized: handle401,\n * onForbidden: handle403,\n * });\n *\n * configurer.applyTo(fetcher);\n * // Now fetcher has all CoSec interceptors configured\n * ```\n */\n applyTo(fetcher: Fetcher): void {\n fetcher.interceptors.request.use(\n new CoSecRequestInterceptor({\n appId: this.config.appId,\n deviceIdStorage: this.deviceIdStorage,\n }),\n );\n\n fetcher.interceptors.request.use(\n new ResourceAttributionRequestInterceptor({\n tokenStorage: this.tokenStorage,\n }),\n );\n if (this.tokenManager) {\n fetcher.interceptors.request.use(\n new AuthorizationRequestInterceptor({\n tokenManager: this.tokenManager,\n }),\n );\n\n fetcher.interceptors.response.use(\n new AuthorizationResponseInterceptor({\n tokenManager: this.tokenManager,\n }),\n );\n }\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","COSEC_REQUEST_INTERCEPTOR_NAME","COSEC_REQUEST_INTERCEPTOR_ORDER","REQUEST_BODY_INTERCEPTOR_ORDER","IGNORE_REFRESH_TOKEN_ATTRIBUTE_KEY","CoSecRequestInterceptor","options","exchange","requestId","deviceId","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","KeyStorage","key","eventBus","BroadcastTypedEventBus","SerialTypedEventBus","reset","typedIdentitySerializer","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","ResourceAttributionRequestInterceptor","tenantId","ownerId","principal","extractedPathParams","tenantIdPathKey","requestPathParams","ownerIdPathKey","parseJwtPayload","parts","base64","paddedBase64","jsonPayload","c","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 +1 @@
1
- {"version":3,"file":"index.umd.js","sources":["../src/types.ts","../src/idGenerator.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 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 * 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 FetchExchange,\n REQUEST_BODY_INTERCEPTOR_ORDER,\n type RequestInterceptor,\n} from '@ahoo-wang/fetcher';\nimport { AppIdCapable, CoSecHeaders, DeviceIdStorageCapable } from './types';\nimport { idGenerator } from './idGenerator';\n\nexport interface CoSecRequestOptions\n extends AppIdCapable,\n DeviceIdStorageCapable {}\n\n/**\n * The name of the CoSecRequestInterceptor.\n */\nexport const COSEC_REQUEST_INTERCEPTOR_NAME = 'CoSecRequestInterceptor';\n\n/**\n * The order of the CoSecRequestInterceptor.\n * Set to REQUEST_BODY_INTERCEPTOR_ORDER + 1000 to ensure it runs after RequestBodyInterceptor.\n */\nexport const COSEC_REQUEST_INTERCEPTOR_ORDER =\n REQUEST_BODY_INTERCEPTOR_ORDER + 1000;\n\nexport const IGNORE_REFRESH_TOKEN_ATTRIBUTE_KEY = 'Ignore-Refresh-Token';\n\n/**\n * Interceptor that automatically adds CoSec authentication headers to requests.\n *\n * This interceptor adds the following headers to each request:\n * - CoSec-Device-Id: Device identifier (stored in localStorage or generated)\n * - CoSec-App-Id: Application identifier\n * - CoSec-Request-Id: Unique request identifier for each request\n *\n * @remarks\n * This interceptor runs after RequestBodyInterceptor but before FetchInterceptor.\n * The order is set to COSEC_REQUEST_INTERCEPTOR_ORDER to ensure it runs after\n * request body processing but before the actual HTTP request is made. This positioning\n * allows for proper authentication header addition after all request body transformations\n * are complete, ensuring that the final request is properly authenticated before\n * being sent over the network.\n */\nexport class CoSecRequestInterceptor implements RequestInterceptor {\n readonly name = COSEC_REQUEST_INTERCEPTOR_NAME;\n readonly order = COSEC_REQUEST_INTERCEPTOR_ORDER;\n private options: CoSecRequestOptions;\n\n /**\n * Creates a new CoSecRequestInterceptor instance.\n * @param options - The CoSec configuration options including appId, deviceIdStorage, and tokenManager\n */\n constructor(options: CoSecRequestOptions) {\n this.options = options;\n }\n\n /**\n * Intercept requests to add CoSec authentication headers.\n *\n * This method adds the following headers to each request:\n * - CoSec-App-Id: The application identifier from the CoSec options\n * - CoSec-Device-Id: A unique device identifier, either retrieved from storage or generated\n * - CoSec-Request-Id: A unique identifier for this specific request\n *\n * @param exchange - The fetch exchange containing the request to process\n *\n * @remarks\n * This method runs after RequestBodyInterceptor but before FetchInterceptor.\n * It ensures that authentication headers are added to the request after all\n * body processing is complete. The positioning allows for proper authentication\n * header addition after all request body transformations are finished, ensuring\n * that the final request is properly authenticated before being sent over the network.\n * This execution order prevents authentication headers from being overwritten by\n * subsequent request processing interceptors.\n *\n * The method also handles token refreshing when the current token is expired but still refreshable.\n * It will attempt to refresh the token before adding the Authorization header to the request.\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.options.deviceIdStorage.getOrCreate();\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.options.appId;\n requestHeaders[CoSecHeaders.DEVICE_ID] = deviceId;\n requestHeaders[CoSecHeaders.REQUEST_ID] = requestId;\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 { FetchExchange, RequestInterceptor } 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\nexport const AUTHORIZATION_REQUEST_INTERCEPTOR_NAME =\n 'AuthorizationRequestInterceptor';\nexport const AUTHORIZATION_REQUEST_INTERCEPTOR_ORDER =\n COSEC_REQUEST_INTERCEPTOR_ORDER + 1000;\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 * 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 { FetchExchange, RequestInterceptor } 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 Number.MAX_SAFE_INTEGER;\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{\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 * @template Payload The type of the JWT payload\n */\nexport interface IJwtToken<Payload extends JwtPayload>\n extends EarlyPeriodCapable {\n readonly token: string;\n readonly payload: Payload | null;\n\n isExpired: boolean;\n}\n\n/**\n * Class representing a JWT token with typed payload\n * @template Payload The type of the JWT payload\n */\nexport class JwtToken<Payload extends JwtPayload>\n implements IJwtToken<Payload>\n{\n public readonly payload: Payload | null;\n\n /**\n * Creates a new JwtToken instance\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 * @returns true if the token is expired, 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\nexport interface RefreshTokenStatusCapable {\n /**\n * Checks if the access token needs to be refreshed\n * @returns true if the access token is expired, false otherwise\n */\n readonly isRefreshNeeded: boolean;\n /**\n * Checks if the refresh token is still valid and can be used to refresh the access token\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 */\nexport class JwtCompositeToken\n implements EarlyPeriodCapable, RefreshTokenStatusCapable\n{\n public readonly access: JwtToken<CoSecJwtPayload>;\n public readonly refresh: JwtToken<JwtPayload>;\n\n /**\n * Creates a new JwtCompositeToken instance\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 * @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 * @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/**\n * Serializer for JwtCompositeToken that handles conversion to and from JSON strings\n */\nexport class JwtCompositeTokenSerializer\n implements Serializer<string, JwtCompositeToken>, EarlyPeriodCapable\n{\n constructor(public readonly earlyPeriod: number = 0) {}\n\n /**\n * Deserializes a JSON string to a JwtCompositeToken\n * @param value The JSON string representation of a composite token\n * @returns A JwtCompositeToken instance\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 * @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\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 { EarlyPeriodCapable } from './jwts';\nimport { KeyStorage, KeyStorageOptions } from '@ahoo-wang/fetcher-storage';\nimport {\n BroadcastTypedEventBus,\n SerialTypedEventBus,\n} from '@ahoo-wang/fetcher-eventbus';\n\nexport const DEFAULT_COSEC_TOKEN_KEY = 'cosec-token';\n\nexport interface TokenStorageOptions\n extends Partial<Omit<KeyStorageOptions<JwtCompositeToken>, 'serializer'>>,\n Partial<EarlyPeriodCapable> {}\n\n/**\n * Storage class for managing access and refresh tokens.\n */\nexport class TokenStorage\n extends KeyStorage<JwtCompositeToken>\n implements EarlyPeriodCapable\n{\n public readonly earlyPeriod: number;\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 setCompositeToken(compositeToken: CompositeToken) {\n this.set(new JwtCompositeToken(compositeToken, this.earlyPeriod));\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';\n\n/**\n * Simplified configuration interface for CoSec setup.\n * Provides flexible configuration with sensible defaults for optional components.\n */\nexport interface CoSecConfig\n extends AppIdCapable,\n Partial<DeviceIdStorageCapable> {\n /**\n * Application ID to be sent in the CoSec-App-Id header.\n * This is required for identifying your application in the CoSec system.\n */\n appId: string;\n\n /**\n * Custom token storage implementation.\n * If not provided, a default TokenStorage instance will be created.\n * Useful for custom storage backends or testing scenarios.\n */\n tokenStorage?: TokenStorage;\n\n /**\n * Custom device ID storage implementation.\n * If not provided, a default DeviceIdStorage instance will be created.\n * Useful for custom device identification strategies or testing scenarios.\n */\n deviceIdStorage?: DeviceIdStorage;\n\n /**\n * Token refresher implementation for handling expired tokens.\n * If not provided, authentication interceptors will not be added.\n * This enables CoSec configuration without full JWT authentication.\n */\n tokenRefresher?: TokenRefresher;\n\n /**\n * Callback function invoked when an unauthorized (401) response is detected.\n * If not provided, 401 errors will not be intercepted.\n */\n onUnauthorized?: (exchange: FetchExchange) => Promise<void> | void;\n\n /**\n * Callback function invoked when a forbidden (403) response is detected.\n * If not provided, 403 errors will not be intercepted.\n */\n onForbidden?: (exchange: FetchExchange) => Promise<void>;\n}\n\n/**\n * CoSecConfigurer provides a flexible way to configure CoSec interceptors\n * and dependencies with a single configuration object.\n *\n * This class implements FetcherConfigurer and supports both full authentication\n * setups and minimal CoSec header injection. It conditionally creates dependencies\n * based on the provided configuration, allowing for different levels of integration.\n *\n * @implements {FetcherConfigurer}\n *\n * @example\n * Full authentication setup with custom storage:\n * ```typescript\n * const configurer = new CoSecConfigurer({\n * appId: 'my-app-001',\n * tokenStorage: new CustomTokenStorage(),\n * deviceIdStorage: new CustomDeviceStorage(),\n * tokenRefresher: {\n * refresh: async (token: CompositeToken) => {\n * const response = await fetch('/api/auth/refresh', {\n * method: 'POST',\n * body: JSON.stringify({ refreshToken: token.refreshToken }),\n * });\n * const newTokens = await response.json();\n * return {\n * accessToken: newTokens.accessToken,\n * refreshToken: newTokens.refreshToken,\n * };\n * },\n * },\n * onUnauthorized: (exchange) => redirectToLogin(),\n * onForbidden: (exchange) => showPermissionError(),\n * });\n *\n * configurer.applyTo(fetcher);\n * ```\n *\n * @example\n * Minimal setup with only CoSec headers (no authentication):\n * ```typescript\n * const configurer = new CoSecConfigurer({\n * appId: 'my-app-001',\n * // No tokenRefresher provided - authentication interceptors won't be added\n * });\n *\n * configurer.applyTo(fetcher);\n * ```\n */\nexport class CoSecConfigurer implements FetcherConfigurer {\n /**\n * Token storage instance, either provided in config or auto-created.\n */\n readonly tokenStorage: TokenStorage;\n\n /**\n * Device ID storage instance, either provided in config or auto-created.\n */\n readonly deviceIdStorage: DeviceIdStorage;\n\n /**\n * JWT token manager instance, only created if tokenRefresher is provided.\n * When undefined, authentication interceptors will not be added.\n */\n readonly tokenManager?: JwtTokenManager;\n\n /**\n * Creates a new CoSecConfigurer instance with the provided configuration.\n *\n * This constructor conditionally creates dependencies based on the configuration:\n * - TokenStorage and DeviceIdStorage are always created (using defaults if not provided)\n * - JwtTokenManager is only created if tokenRefresher is provided\n *\n * @param config - CoSec configuration object\n *\n * @example\n * ```typescript\n * // Full setup with all dependencies\n * const configurer = new CoSecConfigurer({\n * appId: 'my-app',\n * tokenRefresher: myTokenRefresher,\n * });\n *\n * // Minimal setup with custom storage\n * const configurer = new CoSecConfigurer({\n * appId: 'my-app',\n * tokenStorage: customStorage,\n * deviceIdStorage: customDeviceStorage,\n * });\n * ```\n */\n constructor(public readonly config: CoSecConfig) {\n // Create storage instances with fallbacks to defaults\n this.tokenStorage = config.tokenStorage ?? new TokenStorage();\n this.deviceIdStorage = config.deviceIdStorage ?? new DeviceIdStorage();\n\n // Create token manager only if token refresher is provided\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 provided Fetcher instance.\n *\n * This method conditionally configures interceptors based on the provided configuration:\n *\n * Always added:\n * 1. CoSecRequestInterceptor - Adds CoSec headers (appId, deviceId, requestId)\n * 2. ResourceAttributionRequestInterceptor - Adds tenant/owner path parameters\n *\n * Only when `tokenRefresher` is provided:\n * 3. AuthorizationRequestInterceptor - Adds Bearer token authentication\n * 4. AuthorizationResponseInterceptor - Handles token refresh on 401 responses\n *\n * Only when corresponding handlers are provided:\n * 5. UnauthorizedErrorInterceptor - Handles 401 unauthorized errors\n * 6. ForbiddenErrorInterceptor - Handles 403 forbidden errors\n *\n * @param fetcher - The Fetcher instance to configure\n *\n * @example\n * ```typescript\n * const fetcher = new Fetcher({ baseURL: '/api' });\n * const configurer = new CoSecConfigurer({\n * appId: 'my-app',\n * tokenRefresher: myTokenRefresher,\n * onUnauthorized: handle401,\n * onForbidden: handle403,\n * });\n *\n * configurer.applyTo(fetcher);\n * // Now fetcher has all CoSec interceptors configured\n * ```\n */\n applyTo(fetcher: Fetcher): void {\n fetcher.interceptors.request.use(\n new CoSecRequestInterceptor({\n appId: this.config.appId,\n deviceIdStorage: this.deviceIdStorage,\n }),\n );\n\n fetcher.interceptors.request.use(\n new ResourceAttributionRequestInterceptor({\n tokenStorage: this.tokenStorage,\n }),\n );\n if (this.tokenManager) {\n fetcher.interceptors.request.use(\n new AuthorizationRequestInterceptor({\n tokenManager: this.tokenManager,\n }),\n );\n\n fetcher.interceptors.response.use(\n new AuthorizationResponseInterceptor({\n tokenManager: this.tokenManager,\n }),\n );\n }\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 for access tokens.\n */\nexport interface AccessToken {\n accessToken: string;\n}\n\n/**\n * Interface for refresh tokens.\n */\nexport interface RefreshToken {\n refreshToken: string;\n}\n\n/**\n * Composite token interface that contains both access and refresh tokens.\n *\n * accessToken and refreshToken always appear in pairs, no need to split them.\n */\nexport interface CompositeToken extends AccessToken, RefreshToken {}\n\n/**\n * Interface for token refreshers.\n *\n * Provides a method to refresh tokens.\n */\nexport interface TokenRefresher {\n /**\n * Refresh the given token and return a new CompositeToken.\n *\n * @param token The token to refresh\n * @returns A Promise that resolves to a new CompositeToken\n */\n refresh(token: CompositeToken): Promise<CompositeToken>;\n}\n\nexport interface CoSecTokenRefresherOptions {\n fetcher: Fetcher;\n endpoint: string;\n}\n\n/**\n * CoSecTokenRefresher is a class that implements the TokenRefresher interface\n * for refreshing composite tokens through a configured endpoint.\n */\nexport class CoSecTokenRefresher implements TokenRefresher {\n /**\n * Creates a new instance of CoSecTokenRefresher.\n *\n * @param options The configuration options for the token refresher including fetcher and endpoint\n */\n constructor(public readonly options: CoSecTokenRefresherOptions) {}\n\n /**\n * Refresh the given token and return a new CompositeToken.\n *\n * @param token The token to refresh\n * @returns A Promise that resolves to a new CompositeToken\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","COSEC_REQUEST_INTERCEPTOR_NAME","COSEC_REQUEST_INTERCEPTOR_ORDER","REQUEST_BODY_INTERCEPTOR_ORDER","IGNORE_REFRESH_TOKEN_ATTRIBUTE_KEY","CoSecRequestInterceptor","options","exchange","requestId","deviceId","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","KeyStorage","key","eventBus","BroadcastTypedEventBus","SerialTypedEventBus","reset","typedIdentitySerializer","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","ResourceAttributionRequestInterceptor","tenantId","ownerId","principal","extractedPathParams","tenantIdPathKey","requestPathParams","ownerIdPathKey","parseJwtPayload","parts","base64","paddedBase64","jsonPayload","c","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.umd.js","sources":["../src/types.ts","../src/idGenerator.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 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 * 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 FetchExchange,\n REQUEST_BODY_INTERCEPTOR_ORDER,\n type RequestInterceptor,\n} from '@ahoo-wang/fetcher';\nimport { AppIdCapable, CoSecHeaders, DeviceIdStorageCapable } from './types';\nimport { idGenerator } from './idGenerator';\n\nexport interface CoSecRequestOptions\n extends AppIdCapable,\n DeviceIdStorageCapable {}\n\n/**\n * The name of the CoSecRequestInterceptor.\n */\nexport const COSEC_REQUEST_INTERCEPTOR_NAME = 'CoSecRequestInterceptor';\n\n/**\n * The order of the CoSecRequestInterceptor.\n * Set to REQUEST_BODY_INTERCEPTOR_ORDER + 1000 to ensure it runs after RequestBodyInterceptor.\n */\nexport const COSEC_REQUEST_INTERCEPTOR_ORDER =\n REQUEST_BODY_INTERCEPTOR_ORDER + 1000;\n\nexport const IGNORE_REFRESH_TOKEN_ATTRIBUTE_KEY = 'Ignore-Refresh-Token';\n\n/**\n * Interceptor that automatically adds CoSec authentication headers to requests.\n *\n * This interceptor adds the following headers to each request:\n * - CoSec-Device-Id: Device identifier (stored in localStorage or generated)\n * - CoSec-App-Id: Application identifier\n * - CoSec-Request-Id: Unique request identifier for each request\n *\n * @remarks\n * This interceptor runs after RequestBodyInterceptor but before FetchInterceptor.\n * The order is set to COSEC_REQUEST_INTERCEPTOR_ORDER to ensure it runs after\n * request body processing but before the actual HTTP request is made. This positioning\n * allows for proper authentication header addition after all request body transformations\n * are complete, ensuring that the final request is properly authenticated before\n * being sent over the network.\n */\nexport class CoSecRequestInterceptor implements RequestInterceptor {\n readonly name = COSEC_REQUEST_INTERCEPTOR_NAME;\n readonly order = COSEC_REQUEST_INTERCEPTOR_ORDER;\n private options: CoSecRequestOptions;\n\n /**\n * Creates a new CoSecRequestInterceptor instance.\n * @param options - The CoSec configuration options including appId, deviceIdStorage, and tokenManager\n */\n constructor(options: CoSecRequestOptions) {\n this.options = options;\n }\n\n /**\n * Intercept requests to add CoSec authentication headers.\n *\n * This method adds the following headers to each request:\n * - CoSec-App-Id: The application identifier from the CoSec options\n * - CoSec-Device-Id: A unique device identifier, either retrieved from storage or generated\n * - CoSec-Request-Id: A unique identifier for this specific request\n *\n * @param exchange - The fetch exchange containing the request to process\n *\n * @remarks\n * This method runs after RequestBodyInterceptor but before FetchInterceptor.\n * It ensures that authentication headers are added to the request after all\n * body processing is complete. The positioning allows for proper authentication\n * header addition after all request body transformations are finished, ensuring\n * that the final request is properly authenticated before being sent over the network.\n * This execution order prevents authentication headers from being overwritten by\n * subsequent request processing interceptors.\n *\n * The method also handles token refreshing when the current token is expired but still refreshable.\n * It will attempt to refresh the token before adding the Authorization header to the request.\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.options.deviceIdStorage.getOrCreate();\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.options.appId;\n requestHeaders[CoSecHeaders.DEVICE_ID] = deviceId;\n requestHeaders[CoSecHeaders.REQUEST_ID] = requestId;\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 { FetchExchange, RequestInterceptor } 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\nexport const AUTHORIZATION_REQUEST_INTERCEPTOR_NAME =\n 'AuthorizationRequestInterceptor';\nexport const AUTHORIZATION_REQUEST_INTERCEPTOR_ORDER =\n COSEC_REQUEST_INTERCEPTOR_ORDER + 1000;\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 * 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 { FetchExchange, RequestInterceptor } 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 Number.MAX_SAFE_INTEGER;\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{\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 * @template Payload The type of the JWT payload\n */\nexport interface IJwtToken<Payload extends JwtPayload>\n extends EarlyPeriodCapable {\n readonly token: string;\n readonly payload: Payload | null;\n\n isExpired: boolean;\n}\n\n/**\n * Class representing a JWT token with typed payload\n * @template Payload The type of the JWT payload\n */\nexport class JwtToken<Payload extends JwtPayload>\n implements IJwtToken<Payload>\n{\n public readonly payload: Payload | null;\n\n /**\n * Creates a new JwtToken instance\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 * @returns true if the token is expired, 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\nexport interface RefreshTokenStatusCapable {\n /**\n * Checks if the access token needs to be refreshed\n * @returns true if the access token is expired, false otherwise\n */\n readonly isRefreshNeeded: boolean;\n /**\n * Checks if the refresh token is still valid and can be used to refresh the access token\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 */\nexport class JwtCompositeToken\n implements EarlyPeriodCapable, RefreshTokenStatusCapable\n{\n public readonly access: JwtToken<CoSecJwtPayload>;\n public readonly refresh: JwtToken<JwtPayload>;\n\n /**\n * Creates a new JwtCompositeToken instance\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 * @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 * @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/**\n * Serializer for JwtCompositeToken that handles conversion to and from JSON strings\n */\nexport class JwtCompositeTokenSerializer\n implements Serializer<string, JwtCompositeToken>, EarlyPeriodCapable\n{\n constructor(public readonly earlyPeriod: number = 0) {}\n\n /**\n * Deserializes a JSON string to a JwtCompositeToken\n * @param value The JSON string representation of a composite token\n * @returns A JwtCompositeToken instance\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 * @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\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 { EarlyPeriodCapable } from './jwts';\nimport { KeyStorage, KeyStorageOptions } from '@ahoo-wang/fetcher-storage';\nimport {\n BroadcastTypedEventBus,\n SerialTypedEventBus,\n} from '@ahoo-wang/fetcher-eventbus';\n\nexport const DEFAULT_COSEC_TOKEN_KEY = 'cosec-token';\n\nexport interface TokenStorageOptions\n extends Partial<Omit<KeyStorageOptions<JwtCompositeToken>, 'serializer'>>,\n Partial<EarlyPeriodCapable> {}\n\n/**\n * Storage class for managing access and refresh tokens.\n */\nexport class TokenStorage\n extends KeyStorage<JwtCompositeToken>\n implements EarlyPeriodCapable\n{\n public readonly earlyPeriod: number;\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 setCompositeToken(compositeToken: CompositeToken) {\n this.set(new JwtCompositeToken(compositeToken, this.earlyPeriod));\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';\n\n/**\n * Simplified configuration interface for CoSec setup.\n * Provides flexible configuration with sensible defaults for optional components.\n */\nexport interface CoSecConfig\n extends AppIdCapable,\n Partial<DeviceIdStorageCapable> {\n /**\n * Application ID to be sent in the CoSec-App-Id header.\n * This is required for identifying your application in the CoSec system.\n */\n appId: string;\n\n /**\n * Custom token storage implementation.\n * If not provided, a default TokenStorage instance will be created.\n * Useful for custom storage backends or testing scenarios.\n */\n tokenStorage?: TokenStorage;\n\n /**\n * Custom device ID storage implementation.\n * If not provided, a default DeviceIdStorage instance will be created.\n * Useful for custom device identification strategies or testing scenarios.\n */\n deviceIdStorage?: DeviceIdStorage;\n\n /**\n * Token refresher implementation for handling expired tokens.\n * If not provided, authentication interceptors will not be added.\n * This enables CoSec configuration without full JWT authentication.\n */\n tokenRefresher?: TokenRefresher;\n\n /**\n * Callback function invoked when an unauthorized (401) response is detected.\n * If not provided, 401 errors will not be intercepted.\n */\n onUnauthorized?: (exchange: FetchExchange) => Promise<void> | void;\n\n /**\n * Callback function invoked when a forbidden (403) response is detected.\n * If not provided, 403 errors will not be intercepted.\n */\n onForbidden?: (exchange: FetchExchange) => Promise<void>;\n}\n\n/**\n * CoSecConfigurer provides a flexible way to configure CoSec interceptors\n * and dependencies with a single configuration object.\n *\n * This class implements FetcherConfigurer and supports both full authentication\n * setups and minimal CoSec header injection. It conditionally creates dependencies\n * based on the provided configuration, allowing for different levels of integration.\n *\n * @implements {FetcherConfigurer}\n *\n * @example\n * Full authentication setup with custom storage:\n * ```typescript\n * const configurer = new CoSecConfigurer({\n * appId: 'my-app-001',\n * tokenStorage: new CustomTokenStorage(),\n * deviceIdStorage: new CustomDeviceStorage(),\n * tokenRefresher: {\n * refresh: async (token: CompositeToken) => {\n * const response = await fetch('/api/auth/refresh', {\n * method: 'POST',\n * body: JSON.stringify({ refreshToken: token.refreshToken }),\n * });\n * const newTokens = await response.json();\n * return {\n * accessToken: newTokens.accessToken,\n * refreshToken: newTokens.refreshToken,\n * };\n * },\n * },\n * onUnauthorized: (exchange) => redirectToLogin(),\n * onForbidden: (exchange) => showPermissionError(),\n * });\n *\n * configurer.applyTo(fetcher);\n * ```\n *\n * @example\n * Minimal setup with only CoSec headers (no authentication):\n * ```typescript\n * const configurer = new CoSecConfigurer({\n * appId: 'my-app-001',\n * // No tokenRefresher provided - authentication interceptors won't be added\n * });\n *\n * configurer.applyTo(fetcher);\n * ```\n */\nexport class CoSecConfigurer implements FetcherConfigurer {\n /**\n * Token storage instance, either provided in config or auto-created.\n */\n readonly tokenStorage: TokenStorage;\n\n /**\n * Device ID storage instance, either provided in config or auto-created.\n */\n readonly deviceIdStorage: DeviceIdStorage;\n\n /**\n * JWT token manager instance, only created if tokenRefresher is provided.\n * When undefined, authentication interceptors will not be added.\n */\n readonly tokenManager?: JwtTokenManager;\n\n /**\n * Creates a new CoSecConfigurer instance with the provided configuration.\n *\n * This constructor conditionally creates dependencies based on the configuration:\n * - TokenStorage and DeviceIdStorage are always created (using defaults if not provided)\n * - JwtTokenManager is only created if tokenRefresher is provided\n *\n * @param config - CoSec configuration object\n *\n * @example\n * ```typescript\n * // Full setup with all dependencies\n * const configurer = new CoSecConfigurer({\n * appId: 'my-app',\n * tokenRefresher: myTokenRefresher,\n * });\n *\n * // Minimal setup with custom storage\n * const configurer = new CoSecConfigurer({\n * appId: 'my-app',\n * tokenStorage: customStorage,\n * deviceIdStorage: customDeviceStorage,\n * });\n * ```\n */\n constructor(public readonly config: CoSecConfig) {\n // Create storage instances with fallbacks to defaults\n this.tokenStorage = config.tokenStorage ?? new TokenStorage();\n this.deviceIdStorage = config.deviceIdStorage ?? new DeviceIdStorage();\n\n // Create token manager only if token refresher is provided\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 provided Fetcher instance.\n *\n * This method conditionally configures interceptors based on the provided configuration:\n *\n * Always added:\n * 1. CoSecRequestInterceptor - Adds CoSec headers (appId, deviceId, requestId)\n * 2. ResourceAttributionRequestInterceptor - Adds tenant/owner path parameters\n *\n * Only when `tokenRefresher` is provided:\n * 3. AuthorizationRequestInterceptor - Adds Bearer token authentication\n * 4. AuthorizationResponseInterceptor - Handles token refresh on 401 responses\n *\n * Only when corresponding handlers are provided:\n * 5. UnauthorizedErrorInterceptor - Handles 401 unauthorized errors\n * 6. ForbiddenErrorInterceptor - Handles 403 forbidden errors\n *\n * @param fetcher - The Fetcher instance to configure\n *\n * @example\n * ```typescript\n * const fetcher = new Fetcher({ baseURL: '/api' });\n * const configurer = new CoSecConfigurer({\n * appId: 'my-app',\n * tokenRefresher: myTokenRefresher,\n * onUnauthorized: handle401,\n * onForbidden: handle403,\n * });\n *\n * configurer.applyTo(fetcher);\n * // Now fetcher has all CoSec interceptors configured\n * ```\n */\n applyTo(fetcher: Fetcher): void {\n fetcher.interceptors.request.use(\n new CoSecRequestInterceptor({\n appId: this.config.appId,\n deviceIdStorage: this.deviceIdStorage,\n }),\n );\n\n fetcher.interceptors.request.use(\n new ResourceAttributionRequestInterceptor({\n tokenStorage: this.tokenStorage,\n }),\n );\n if (this.tokenManager) {\n fetcher.interceptors.request.use(\n new AuthorizationRequestInterceptor({\n tokenManager: this.tokenManager,\n }),\n );\n\n fetcher.interceptors.response.use(\n new AuthorizationResponseInterceptor({\n tokenManager: this.tokenManager,\n }),\n );\n }\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","COSEC_REQUEST_INTERCEPTOR_NAME","COSEC_REQUEST_INTERCEPTOR_ORDER","REQUEST_BODY_INTERCEPTOR_ORDER","IGNORE_REFRESH_TOKEN_ATTRIBUTE_KEY","CoSecRequestInterceptor","options","exchange","requestId","deviceId","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","KeyStorage","key","eventBus","BroadcastTypedEventBus","SerialTypedEventBus","reset","typedIdentitySerializer","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","ResourceAttributionRequestInterceptor","tenantId","ownerId","principal","extractedPathParams","tenantIdPathKey","requestPathParams","ownerIdPathKey","parseJwtPayload","parts","base64","paddedBase64","jsonPayload","c","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,12 +1,24 @@
1
1
  import { Fetcher } from '@ahoo-wang/fetcher';
2
2
  /**
3
- * Interface for access tokens.
3
+ * Interface representing an access token used for authentication.
4
+ *
5
+ * This interface defines the structure for access tokens, which are typically
6
+ * short-lived tokens used to authenticate API requests.
7
+ *
8
+ * @interface AccessToken
9
+ * @property {string} accessToken - The access token string used for authentication.
4
10
  */
5
11
  export interface AccessToken {
6
12
  accessToken: string;
7
13
  }
8
14
  /**
9
- * Interface for refresh tokens.
15
+ * Interface representing a refresh token used to obtain new access tokens.
16
+ *
17
+ * Refresh tokens are long-lived tokens that can be used to request new access
18
+ * tokens without requiring the user to re-authenticate.
19
+ *
20
+ * @interface RefreshToken
21
+ * @property {string} refreshToken - The refresh token string used to obtain new access tokens.
10
22
  */
11
23
  export interface RefreshToken {
12
24
  refreshToken: string;
@@ -14,45 +26,148 @@ export interface RefreshToken {
14
26
  /**
15
27
  * Composite token interface that contains both access and refresh tokens.
16
28
  *
17
- * accessToken and refreshToken always appear in pairs, no need to split them.
29
+ * This interface combines AccessToken and RefreshToken, ensuring that both
30
+ * tokens are always handled together. In authentication systems, access and
31
+ * refresh tokens are typically issued and refreshed as a pair.
32
+ *
33
+ * @interface CompositeToken
34
+ * @extends AccessToken
35
+ * @extends RefreshToken
36
+ * @property {string} accessToken - The access token string used for authentication.
37
+ * @property {string} refreshToken - The refresh token string used to obtain new access tokens.
18
38
  */
19
39
  export interface CompositeToken extends AccessToken, RefreshToken {
20
40
  }
21
41
  /**
22
42
  * Interface for token refreshers.
23
43
  *
24
- * Provides a method to refresh tokens.
44
+ * This interface defines the contract for classes responsible for refreshing
45
+ * authentication tokens. Implementations should handle the logic of exchanging
46
+ * an expired or soon-to-expire token for a new one.
47
+ *
48
+ * **CRITICAL**: When implementing the `refresh` method, always include the
49
+ * `attributes: new Map([[IGNORE_REFRESH_TOKEN_ATTRIBUTE_KEY, true]])` in the
50
+ * request options to prevent infinite loops. Without this attribute, the request
51
+ * interceptor may attempt to refresh the token recursively, causing a deadlock.
52
+ *
53
+ * @interface TokenRefresher
25
54
  */
26
55
  export interface TokenRefresher {
27
56
  /**
28
- * Refresh the given token and return a new CompositeToken.
57
+ * Refreshes the given composite token and returns a new one.
29
58
  *
30
- * @param token The token to refresh
31
- * @returns A Promise that resolves to a new CompositeToken
59
+ * This method should send the provided token to the appropriate endpoint
60
+ * and return the refreshed token pair. **IMPORTANT**: The implementation must
61
+ * include `attributes: new Map([[IGNORE_REFRESH_TOKEN_ATTRIBUTE_KEY, true]])`
62
+ * in the request options to prevent recursive refresh attempts that could
63
+ * cause infinite loops.
64
+ *
65
+ * @param {CompositeToken} token - The composite token to refresh, containing both access and refresh tokens.
66
+ * @returns {Promise<CompositeToken>} A promise that resolves to a new CompositeToken with updated access and refresh tokens.
67
+ * @throws {Error} Throws an error if the token refresh fails due to network issues, invalid tokens, or server errors.
68
+ *
69
+ * @example
70
+ * ```typescript
71
+ * const refresher: TokenRefresher = new CoSecTokenRefresher({ fetcher, endpoint: '/auth/refresh' });
72
+ * const newToken = await refresher.refresh(currentToken);
73
+ * console.log('New access token:', newToken.accessToken);
74
+ * ```
75
+ *
76
+ * @warning **Critical**: Failing to set the `IGNORE_REFRESH_TOKEN_ATTRIBUTE_KEY`
77
+ * attribute will result in infinite loops during token refresh operations.
32
78
  */
33
79
  refresh(token: CompositeToken): Promise<CompositeToken>;
34
80
  }
81
+ /**
82
+ * Configuration options for the CoSecTokenRefresher class.
83
+ *
84
+ * This interface defines the required options to initialize a CoSecTokenRefresher
85
+ * instance, including the HTTP client and the refresh endpoint URL.
86
+ *
87
+ * @interface CoSecTokenRefresherOptions
88
+ * @property {Fetcher} fetcher - The HTTP client instance used to make requests to the refresh endpoint.
89
+ * @property {string} endpoint - The URL endpoint where token refresh requests are sent.
90
+ */
35
91
  export interface CoSecTokenRefresherOptions {
36
92
  fetcher: Fetcher;
37
93
  endpoint: string;
38
94
  }
39
95
  /**
40
- * CoSecTokenRefresher is a class that implements the TokenRefresher interface
41
- * for refreshing composite tokens through a configured endpoint.
96
+ * Implementation of TokenRefresher for refreshing composite tokens using CoSec authentication.
97
+ *
98
+ * This class provides a concrete implementation of the TokenRefresher interface,
99
+ * using the provided Fetcher instance to send POST requests to a specified endpoint
100
+ * for token refresh operations. It is designed to work with CoSec (Corporate Security)
101
+ * authentication systems.
102
+ *
103
+ * @class CoSecTokenRefresher
104
+ * @implements TokenRefresher
105
+ *
106
+ * @example
107
+ * ```typescript
108
+ * import { Fetcher } from '@ahoo-wang/fetcher';
109
+ * import { CoSecTokenRefresher } from './tokenRefresher';
110
+ *
111
+ * const fetcher = new Fetcher();
112
+ * const refresher = new CoSecTokenRefresher({
113
+ * fetcher,
114
+ * endpoint: 'https://api.example.com/auth/refresh'
115
+ * });
116
+ *
117
+ * const currentToken = { accessToken: 'old-token', refreshToken: 'refresh-token' };
118
+ * const newToken = await refresher.refresh(currentToken);
119
+ * ```
42
120
  */
43
121
  export declare class CoSecTokenRefresher implements TokenRefresher {
44
122
  readonly options: CoSecTokenRefresherOptions;
45
123
  /**
46
124
  * Creates a new instance of CoSecTokenRefresher.
47
125
  *
48
- * @param options The configuration options for the token refresher including fetcher and endpoint
126
+ * @param {CoSecTokenRefresherOptions} options - The configuration options for the token refresher.
127
+ * @param {Fetcher} options.fetcher - The HTTP client instance used for making refresh requests.
128
+ * @param {string} options.endpoint - The URL endpoint for token refresh operations.
129
+ *
130
+ * @example
131
+ * ```typescript
132
+ * const refresher = new CoSecTokenRefresher({
133
+ * fetcher: myFetcherInstance,
134
+ * endpoint: '/api/v1/auth/refresh'
135
+ * });
136
+ * ```
49
137
  */
50
138
  constructor(options: CoSecTokenRefresherOptions);
51
139
  /**
52
- * Refresh the given token and return a new CompositeToken.
140
+ * Refreshes the given composite token by sending a POST request to the configured endpoint.
141
+ *
142
+ * This method sends the current token pair to the refresh endpoint and expects
143
+ * a new CompositeToken in response.
144
+ *
145
+ * **CRITICAL**: The request includes a special
146
+ * attribute `attributes: new Map([[IGNORE_REFRESH_TOKEN_ATTRIBUTE_KEY, true]])`
147
+ * to prevent infinite loops. Without this attribute, the request interceptor
148
+ * may attempt to refresh the token again, causing a recursive loop.
149
+ *
150
+ * @param {CompositeToken} token - The composite token to refresh, containing both access and refresh tokens.
151
+ * @returns {Promise<CompositeToken>} A promise that resolves to a new CompositeToken with refreshed tokens.
152
+ * @throws {Error} Throws an error if the HTTP request fails, the server returns an error status, or the response cannot be parsed as JSON.
153
+ * @throws {NetworkError} Throws a network error if there are connectivity issues.
154
+ * @throws {AuthenticationError} Throws an authentication error if the refresh token is invalid or expired.
155
+ *
156
+ * @example
157
+ * ```typescript
158
+ * const refresher = new CoSecTokenRefresher({ fetcher, endpoint: '/auth/refresh' });
159
+ * const token = { accessToken: 'expired-token', refreshToken: 'valid-refresh-token' };
160
+ *
161
+ * try {
162
+ * const newToken = await refresher.refresh(token);
163
+ * console.log('Refreshed access token:', newToken.accessToken);
164
+ * } catch (error) {
165
+ * console.error('Token refresh failed:', error.message);
166
+ * }
167
+ * ```
53
168
  *
54
- * @param token The token to refresh
55
- * @returns A Promise that resolves to a new CompositeToken
169
+ * @warning **Important**: Always include the `IGNORE_REFRESH_TOKEN_ATTRIBUTE_KEY` attribute
170
+ * in refresh requests to avoid infinite loops caused by recursive token refresh attempts.
56
171
  */
57
172
  refresh(token: CompositeToken): Promise<CompositeToken>;
58
173
  }
@@ -1 +1 @@
1
- {"version":3,"file":"tokenRefresher.d.ts","sourceRoot":"","sources":["../src/tokenRefresher.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,OAAO,EAAoB,MAAM,oBAAoB,CAAC;AAG/D;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,YAAY,EAAE,MAAM,CAAC;CACtB;AAED;;;;GAIG;AACH,MAAM,WAAW,cAAe,SAAQ,WAAW,EAAE,YAAY;CAAG;AAEpE;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B;;;;;OAKG;IACH,OAAO,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;CACzD;AAED,MAAM,WAAW,0BAA0B;IACzC,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;GAGG;AACH,qBAAa,mBAAoB,YAAW,cAAc;aAM5B,OAAO,EAAE,0BAA0B;IAL/D;;;;OAIG;gBACyB,OAAO,EAAE,0BAA0B;IAE/D;;;;;OAKG;IACH,OAAO,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;CAexD"}
1
+ {"version":3,"file":"tokenRefresher.d.ts","sourceRoot":"","sources":["../src/tokenRefresher.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,OAAO,EAAoB,MAAM,oBAAoB,CAAC;AAG/D;;;;;;;;GAQG;AACH,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,YAAY;IAC3B,YAAY,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,cAAe,SAAQ,WAAW,EAAE,YAAY;CAAG;AAEpE;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,cAAc;IAC7B;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,OAAO,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;CACzD;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,0BAA0B;IACzC,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,qBAAa,mBAAoB,YAAW,cAAc;aAgB5B,OAAO,EAAE,0BAA0B;IAf/D;;;;;;;;;;;;;;OAcG;gBACyB,OAAO,EAAE,0BAA0B;IAE/D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAgCG;IACH,OAAO,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;CAexD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ahoo-wang/fetcher-cosec",
3
- "version": "3.3.6",
3
+ "version": "3.3.8",
4
4
  "description": "Enterprise-grade CoSec authentication integration for the Fetcher HTTP client with comprehensive security features including automatic token management, device tracking, and request attribution.",
5
5
  "keywords": [
6
6
  "fetch",