@lgriffin/esi.ts 10.2.1 → 11.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +399 -33
- package/LICENSE +667 -19
- package/NOTICE +19 -0
- package/README.md +149 -902
- package/dist/EsiClient.d.mts +5 -36
- package/dist/EsiClient.d.ts +5 -36
- package/dist/EsiClient.d.ts.map +1 -1
- package/dist/EsiClientBuilder.d.mts +128 -1
- package/dist/EsiClientBuilder.d.ts +128 -1
- package/dist/EsiClientBuilder.d.ts.map +1 -1
- package/dist/adapters/PipelineTransport.d.ts +16 -0
- package/dist/adapters/PipelineTransport.d.ts.map +1 -0
- package/dist/auth/EsiTokenManager.d.mts +62 -21
- package/dist/auth/EsiTokenManager.d.ts +62 -21
- package/dist/auth/EsiTokenManager.d.ts.map +1 -1
- package/dist/auth/EveSsoClient.d.mts +9 -9
- package/dist/auth/EveSsoClient.d.ts +9 -9
- package/dist/auth/EveSsoClient.d.ts.map +1 -1
- package/dist/auth/errors.d.mts +2 -2
- package/dist/auth/errors.d.ts +2 -2
- package/dist/auth/errors.d.ts.map +1 -1
- package/dist/auth/jwt.d.mts +7 -7
- package/dist/auth/jwt.d.ts +7 -7
- package/dist/auth/jwt.d.ts.map +1 -1
- package/dist/auth/storage/FileTokenStorage.d.mts +1 -1
- package/dist/auth/storage/FileTokenStorage.d.ts +1 -1
- package/dist/auth/storage/FileTokenStorage.d.ts.map +1 -1
- package/dist/{chunk-BBSHKVPI.js → chunk-CM63VGGF.js} +1 -1
- package/dist/chunk-CM63VGGF.js.map +1 -0
- package/dist/chunk-DFRTIZK4.js +275 -0
- package/dist/chunk-DFRTIZK4.js.map +1 -0
- package/dist/{chunk-VZS32UQ3.mjs → chunk-HXS7ZVEL.mjs} +129 -28
- package/dist/chunk-HXS7ZVEL.mjs.map +1 -0
- package/dist/chunk-JNFQXEWZ.mjs +275 -0
- package/dist/chunk-JNFQXEWZ.mjs.map +1 -0
- package/dist/chunk-KIY3XA5Q.mjs +2980 -0
- package/dist/chunk-KIY3XA5Q.mjs.map +1 -0
- package/dist/{chunk-R7D7M2LQ.mjs → chunk-MGOE2YNH.mjs} +1 -1
- package/dist/chunk-MGOE2YNH.mjs.map +1 -0
- package/dist/{chunk-SAQBXXTM.js → chunk-MRKZ55XI.js} +130 -29
- package/dist/chunk-MRKZ55XI.js.map +1 -0
- package/dist/chunk-NKFIHGC6.js +2980 -0
- package/dist/chunk-NKFIHGC6.js.map +1 -0
- package/dist/{chunk-MSJHIJXA.js → chunk-S73PKM2Y.js} +20 -5
- package/dist/chunk-S73PKM2Y.js.map +1 -0
- package/dist/{chunk-3ZI5A37L.mjs → chunk-YUHGSPQ7.mjs} +19 -4
- package/dist/chunk-YUHGSPQ7.mjs.map +1 -0
- package/dist/client/identity.d.mts +20 -0
- package/dist/client/identity.d.ts +20 -0
- package/dist/client/identity.d.ts.map +1 -0
- package/dist/client/index.d.mts +11 -0
- package/dist/client/index.d.ts +11 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +6607 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/index.mjs +6607 -0
- package/dist/client/index.mjs.map +1 -0
- package/dist/client/runtime.d.mts +67 -0
- package/dist/client/runtime.d.ts +67 -0
- package/dist/client/runtime.d.ts.map +1 -0
- package/dist/clients/BaseEsiClient.d.ts.map +1 -1
- package/dist/clients/ClientRegistry.d.mts +45 -0
- package/dist/clients/ClientRegistry.d.ts +45 -0
- package/dist/clients/ClientRegistry.d.ts.map +1 -0
- package/dist/clients/CorporationsClient.d.mts +3 -3
- package/dist/clients/CorporationsClient.d.ts +3 -3
- package/dist/clients/CorporationsClient.d.ts.map +1 -1
- package/dist/clients/MailClient.d.mts +2 -2
- package/dist/clients/MailClient.d.ts +2 -2
- package/dist/clients/MailClient.d.ts.map +1 -1
- package/dist/clients/MetaClient.d.mts +3 -2
- package/dist/clients/MetaClient.d.ts +3 -2
- package/dist/clients/MetaClient.d.ts.map +1 -1
- package/dist/clients/MilitaryCampaignsClient.d.mts +14 -7
- package/dist/clients/MilitaryCampaignsClient.d.ts +14 -7
- package/dist/clients/MilitaryCampaignsClient.d.ts.map +1 -1
- package/dist/clients/RouteClient.d.mts +4 -4
- package/dist/clients/RouteClient.d.ts +4 -4
- package/dist/clients/RouteClient.d.ts.map +1 -1
- package/dist/clients/SkillsClient.d.mts +1 -1
- package/dist/clients/SkillsClient.d.ts +1 -1
- package/dist/clients/SkillsClient.d.ts.map +1 -1
- package/dist/clients/SkyhooksClient.d.mts +4 -3
- package/dist/clients/SkyhooksClient.d.ts +4 -3
- package/dist/clients/SkyhooksClient.d.ts.map +1 -1
- package/dist/core/ApiClient.d.mts +11 -0
- package/dist/core/ApiClient.d.ts +11 -0
- package/dist/core/ApiClient.d.ts.map +1 -1
- package/dist/core/ApiClientBuilder.d.ts.map +1 -1
- package/dist/core/ApiRequestHandler.d.ts +2 -1
- package/dist/core/ApiRequestHandler.d.ts.map +1 -1
- package/dist/core/BatchRequestHandler.d.mts +13 -2
- package/dist/core/BatchRequestHandler.d.ts +13 -2
- package/dist/core/BatchRequestHandler.d.ts.map +1 -1
- package/dist/core/EsiClientConfig.d.mts +60 -0
- package/dist/core/EsiClientConfig.d.ts +60 -0
- package/dist/core/EsiClientConfig.d.ts.map +1 -0
- package/dist/core/RetryStrategy.d.mts +3 -3
- package/dist/core/RetryStrategy.d.ts +3 -3
- package/dist/core/RetryStrategy.d.ts.map +1 -1
- package/dist/core/cache/ETagCacheManager.d.mts +9 -4
- package/dist/core/cache/ETagCacheManager.d.ts +9 -4
- package/dist/core/cache/ETagCacheManager.d.ts.map +1 -1
- package/dist/core/cache/cacheKey.d.ts +31 -6
- package/dist/core/cache/cacheKey.d.ts.map +1 -1
- package/dist/core/circuitBreaker/CircuitBreaker.d.mts +8 -12
- package/dist/core/circuitBreaker/CircuitBreaker.d.ts +8 -12
- package/dist/core/circuitBreaker/CircuitBreaker.d.ts.map +1 -1
- package/dist/core/clock.d.ts +8 -0
- package/dist/core/clock.d.ts.map +1 -0
- package/dist/core/configureApiClient.d.mts +1 -1
- package/dist/core/configureApiClient.d.ts +1 -1
- package/dist/core/configureApiClient.d.ts.map +1 -1
- package/dist/core/constants.d.ts +3 -3
- package/dist/core/constants.d.ts.map +1 -1
- package/dist/core/endpoints/EndpointDefinition.d.mts +17 -0
- package/dist/core/endpoints/EndpointDefinition.d.ts +17 -0
- package/dist/core/endpoints/EndpointDefinition.d.ts.map +1 -1
- package/dist/core/endpoints/assetEndpoints.d.mts +12 -0
- package/dist/core/endpoints/assetEndpoints.d.ts +12 -0
- package/dist/core/endpoints/assetEndpoints.d.ts.map +1 -1
- package/dist/core/endpoints/characterEndpoints.d.mts +10 -0
- package/dist/core/endpoints/characterEndpoints.d.ts +10 -0
- package/dist/core/endpoints/characterEndpoints.d.ts.map +1 -1
- package/dist/core/endpoints/contactEndpoints.d.mts +1 -0
- package/dist/core/endpoints/contactEndpoints.d.ts +1 -0
- package/dist/core/endpoints/contactEndpoints.d.ts.map +1 -1
- package/dist/core/endpoints/contractEndpoints.d.mts +2 -0
- package/dist/core/endpoints/contractEndpoints.d.ts +2 -0
- package/dist/core/endpoints/contractEndpoints.d.ts.map +1 -1
- package/dist/core/endpoints/corporationEndpoints.d.mts +16 -3
- package/dist/core/endpoints/corporationEndpoints.d.ts +16 -3
- package/dist/core/endpoints/corporationEndpoints.d.ts.map +1 -1
- package/dist/core/endpoints/createClient.d.mts +2 -2
- package/dist/core/endpoints/createClient.d.ts +2 -2
- package/dist/core/endpoints/createClient.d.ts.map +1 -1
- package/dist/core/endpoints/esi-cache-ttls.generated.d.ts.map +1 -1
- package/dist/core/endpoints/esi-rate-limit-groups.generated.d.ts.map +1 -1
- package/dist/core/endpoints/esi-scopes.generated.d.ts.map +1 -1
- package/dist/core/endpoints/fittingEndpoints.d.mts +3 -0
- package/dist/core/endpoints/fittingEndpoints.d.ts +3 -0
- package/dist/core/endpoints/fittingEndpoints.d.ts.map +1 -1
- package/dist/core/endpoints/fleetEndpoints.d.mts +6 -0
- package/dist/core/endpoints/fleetEndpoints.d.ts +6 -0
- package/dist/core/endpoints/fleetEndpoints.d.ts.map +1 -1
- package/dist/core/endpoints/industryEndpoints.d.mts +2 -2
- package/dist/core/endpoints/industryEndpoints.d.ts +2 -2
- package/dist/core/endpoints/mailEndpoints.d.mts +2 -0
- package/dist/core/endpoints/mailEndpoints.d.ts +2 -0
- package/dist/core/endpoints/mailEndpoints.d.ts.map +1 -1
- package/dist/core/endpoints/mercenaryEndpoints.d.mts +1 -1
- package/dist/core/endpoints/mercenaryEndpoints.d.ts +1 -1
- package/dist/core/endpoints/metaEndpoints.d.mts +23 -8
- package/dist/core/endpoints/metaEndpoints.d.ts +23 -8
- package/dist/core/endpoints/metaEndpoints.d.ts.map +1 -1
- package/dist/core/endpoints/militaryCampaignEndpoints.d.mts +78 -49
- package/dist/core/endpoints/militaryCampaignEndpoints.d.ts +78 -49
- package/dist/core/endpoints/militaryCampaignEndpoints.d.ts.map +1 -1
- package/dist/core/endpoints/paragonHubEndpoints.d.mts +5 -5
- package/dist/core/endpoints/paragonHubEndpoints.d.ts +5 -5
- package/dist/core/endpoints/skyhookEndpoints.d.mts +10 -8
- package/dist/core/endpoints/skyhookEndpoints.d.ts +10 -8
- package/dist/core/endpoints/skyhookEndpoints.d.ts.map +1 -1
- package/dist/core/logger/DefaultLogger.d.mts +20 -1
- package/dist/core/logger/DefaultLogger.d.ts +20 -1
- package/dist/core/logger/DefaultLogger.d.ts.map +1 -1
- package/dist/core/logger/ILogger.d.mts +9 -0
- package/dist/core/logger/ILogger.d.ts +9 -0
- package/dist/core/logger/ILogger.d.ts.map +1 -1
- package/dist/core/logger/NoopLogger.d.ts.map +1 -1
- package/dist/core/logger/clientLog.d.ts.map +1 -1
- package/dist/core/logger/loggerUtil.d.ts.map +1 -1
- package/dist/core/logger/redactLog.d.ts +23 -0
- package/dist/core/logger/redactLog.d.ts.map +1 -0
- package/dist/core/pagination/CursorPaginationHandler.d.mts +2 -1
- package/dist/core/pagination/CursorPaginationHandler.d.ts +2 -1
- package/dist/core/pagination/CursorPaginationHandler.d.ts.map +1 -1
- package/dist/core/ports/CacheStore.d.mts +32 -0
- package/dist/core/ports/CacheStore.d.ts +32 -0
- package/dist/core/ports/CacheStore.d.ts.map +1 -0
- package/dist/core/ports/Clock.d.mts +13 -0
- package/dist/core/ports/Clock.d.ts +13 -0
- package/dist/core/ports/Clock.d.ts.map +1 -0
- package/dist/core/ports/HttpTransport.d.mts +7 -0
- package/dist/core/ports/HttpTransport.d.ts +7 -0
- package/dist/core/ports/HttpTransport.d.ts.map +1 -0
- package/dist/core/ports/Identity.d.mts +31 -0
- package/dist/core/ports/Identity.d.ts +31 -0
- package/dist/core/ports/Identity.d.ts.map +1 -0
- package/dist/core/ports/Logger.d.mts +17 -0
- package/dist/core/ports/Logger.d.ts +17 -0
- package/dist/core/ports/Logger.d.ts.map +1 -0
- package/dist/core/ports/OperationTransport.d.mts +42 -0
- package/dist/core/ports/OperationTransport.d.ts +42 -0
- package/dist/core/ports/OperationTransport.d.ts.map +1 -0
- package/dist/core/ports/TokenProvider.d.mts +7 -0
- package/dist/core/ports/TokenProvider.d.ts +7 -0
- package/dist/core/ports/TokenProvider.d.ts.map +1 -0
- package/dist/core/ports/index.d.mts +14 -0
- package/dist/core/ports/index.d.ts +14 -0
- package/dist/core/ports/index.d.ts.map +1 -0
- package/dist/core/rateLimiter/RateLimiter.d.mts +11 -4
- package/dist/core/rateLimiter/RateLimiter.d.ts +11 -4
- package/dist/core/rateLimiter/RateLimiter.d.ts.map +1 -1
- package/dist/core/requestPipeline/cachePolicy.d.ts +5 -3
- package/dist/core/requestPipeline/cachePolicy.d.ts.map +1 -1
- package/dist/core/requestPipeline/fetchExecution.d.ts +3 -1
- package/dist/core/requestPipeline/fetchExecution.d.ts.map +1 -1
- package/dist/core/requestPipeline/headers.d.ts +1 -1
- package/dist/core/requestPipeline/headers.d.ts.map +1 -1
- package/dist/core/requestPipeline/index.d.ts +3 -5
- package/dist/core/requestPipeline/index.d.ts.map +1 -1
- package/dist/core/requestPipeline/paginationOrchestration.d.ts +1 -1
- package/dist/core/requestPipeline/paginationOrchestration.d.ts.map +1 -1
- package/dist/core/requestPipeline/statusHandling.d.ts +3 -3
- package/dist/core/requestPipeline/statusHandling.d.ts.map +1 -1
- package/dist/core/util/callerIdentity.d.ts +10 -0
- package/dist/core/util/callerIdentity.d.ts.map +1 -0
- package/dist/core/util/concurrency.d.ts +2 -2
- package/dist/core/util/concurrency.d.ts.map +1 -1
- package/dist/core/util/error.d.mts +92 -3
- package/dist/core/util/error.d.ts +92 -3
- package/dist/core/util/error.d.ts.map +1 -1
- package/dist/core/util/retry.d.mts +4 -4
- package/dist/core/util/retry.d.ts +4 -4
- package/dist/core/util/retry.d.ts.map +1 -1
- package/dist/core/util/validation.d.ts +6 -0
- package/dist/core/util/validation.d.ts.map +1 -1
- package/dist/errors.d.mts +2 -3
- package/dist/errors.d.ts +2 -3
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +29 -3
- package/dist/errors.js.map +1 -1
- package/dist/errors.mjs +28 -2
- package/dist/generated/operations.generated.d.mts +9379 -0
- package/dist/generated/operations.generated.d.ts +9379 -0
- package/dist/generated/operations.generated.d.ts.map +1 -0
- package/dist/index.d.mts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +699 -2864
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +358 -2523
- package/dist/index.mjs.map +1 -1
- package/dist/schemas/character.d.mts +5 -0
- package/dist/schemas/character.d.ts +5 -0
- package/dist/schemas/character.d.ts.map +1 -1
- package/dist/schemas/contacts.d.mts +2 -0
- package/dist/schemas/contacts.d.ts +2 -0
- package/dist/schemas/contacts.d.ts.map +1 -1
- package/dist/schemas/corporation.d.mts +24 -3
- package/dist/schemas/corporation.d.ts +24 -3
- package/dist/schemas/corporation.d.ts.map +1 -1
- package/dist/schemas/fittings.d.mts +4 -0
- package/dist/schemas/fittings.d.ts +4 -0
- package/dist/schemas/fittings.d.ts.map +1 -1
- package/dist/schemas/fleet.d.mts +8 -0
- package/dist/schemas/fleet.d.ts +8 -0
- package/dist/schemas/fleet.d.ts.map +1 -1
- package/dist/schemas/index.js +24 -2
- package/dist/schemas/index.js.map +1 -1
- package/dist/schemas/index.mjs +23 -1
- package/dist/schemas/industry.d.mts +2 -2
- package/dist/schemas/industry.d.ts +2 -2
- package/dist/schemas/mail.d.mts +4 -0
- package/dist/schemas/mail.d.ts +4 -0
- package/dist/schemas/mail.d.ts.map +1 -1
- package/dist/schemas/mercenary.d.mts +1 -1
- package/dist/schemas/mercenary.d.ts +1 -1
- package/dist/schemas/meta.d.mts +18 -9
- package/dist/schemas/meta.d.ts +18 -9
- package/dist/schemas/meta.d.ts.map +1 -1
- package/dist/schemas/military-campaigns.d.mts +54 -10
- package/dist/schemas/military-campaigns.d.ts +54 -10
- package/dist/schemas/military-campaigns.d.ts.map +1 -1
- package/dist/schemas/paragon-hub.d.mts +4 -4
- package/dist/schemas/paragon-hub.d.ts +4 -4
- package/dist/schemas/skyhooks.d.mts +18 -6
- package/dist/schemas/skyhooks.d.ts +18 -6
- package/dist/schemas/skyhooks.d.ts.map +1 -1
- package/dist/sde/clock.d.ts +11 -0
- package/dist/sde/clock.d.ts.map +1 -0
- package/dist/sde/domain/characters/schemas.d.ts +149 -0
- package/dist/sde/domain/characters/schemas.d.ts.map +1 -0
- package/dist/sde/domain/characters/types.d.mts +163 -0
- package/dist/sde/domain/characters/types.d.ts +163 -0
- package/dist/sde/domain/characters/types.d.ts.map +1 -0
- package/dist/sde/domain/content/schemas.d.ts +84 -0
- package/dist/sde/domain/content/schemas.d.ts.map +1 -0
- package/dist/sde/domain/content/types.d.mts +91 -0
- package/dist/sde/domain/content/types.d.ts +91 -0
- package/dist/sde/domain/content/types.d.ts.map +1 -0
- package/dist/sde/domain/corporations/schemas.d.ts +113 -0
- package/dist/sde/domain/corporations/schemas.d.ts.map +1 -0
- package/dist/sde/domain/corporations/types.d.mts +118 -0
- package/dist/sde/domain/corporations/types.d.ts +118 -0
- package/dist/sde/domain/corporations/types.d.ts.map +1 -0
- package/dist/sde/domain/dogma/schemas.d.ts +76 -0
- package/dist/sde/domain/dogma/schemas.d.ts.map +1 -0
- package/dist/sde/domain/dogma/types.d.mts +80 -0
- package/dist/sde/domain/dogma/types.d.ts +80 -0
- package/dist/sde/domain/dogma/types.d.ts.map +1 -0
- package/dist/sde/domain/industry/schemas.d.ts +204 -0
- package/dist/sde/domain/industry/schemas.d.ts.map +1 -0
- package/dist/sde/domain/industry/types.d.mts +88 -0
- package/dist/sde/domain/industry/types.d.ts +88 -0
- package/dist/sde/domain/industry/types.d.ts.map +1 -0
- package/dist/sde/domain/market/schemas.d.ts +24 -0
- package/dist/sde/domain/market/schemas.d.ts.map +1 -0
- package/dist/sde/domain/market/types.d.mts +25 -0
- package/dist/sde/domain/market/types.d.ts +25 -0
- package/dist/sde/domain/market/types.d.ts.map +1 -0
- package/dist/sde/domain/schemas.d.ts +15 -0
- package/dist/sde/domain/schemas.d.ts.map +1 -0
- package/dist/sde/domain/skins/schemas.d.ts +109 -0
- package/dist/sde/domain/skins/schemas.d.ts.map +1 -0
- package/dist/sde/domain/skins/types.d.mts +121 -0
- package/dist/sde/domain/skins/types.d.ts +121 -0
- package/dist/sde/domain/skins/types.d.ts.map +1 -0
- package/dist/sde/domain/types/schemas.d.ts +163 -0
- package/dist/sde/domain/types/schemas.d.ts.map +1 -0
- package/dist/sde/domain/types/types.d.mts +182 -0
- package/dist/sde/domain/types/types.d.ts +182 -0
- package/dist/sde/domain/types/types.d.ts.map +1 -0
- package/dist/sde/domain/types.d.mts +16 -0
- package/dist/sde/domain/types.d.ts +16 -0
- package/dist/sde/domain/types.d.ts.map +1 -0
- package/dist/sde/domain/ui/schemas.d.ts +42 -0
- package/dist/sde/domain/ui/schemas.d.ts.map +1 -0
- package/dist/sde/domain/ui/types.d.mts +45 -0
- package/dist/sde/domain/ui/types.d.ts +45 -0
- package/dist/sde/domain/ui/types.d.ts.map +1 -0
- package/dist/sde/domain/universe/schemas.d.ts +239 -0
- package/dist/sde/domain/universe/schemas.d.ts.map +1 -0
- package/dist/sde/domain/universe/types.d.mts +206 -0
- package/dist/sde/domain/universe/types.d.ts +206 -0
- package/dist/sde/domain/universe/types.d.ts.map +1 -0
- package/dist/sde/domain/version/schemas.d.ts +11 -0
- package/dist/sde/domain/version/schemas.d.ts.map +1 -0
- package/dist/sde/errors.d.mts +1 -1
- package/dist/sde/errors.d.ts +1 -1
- package/dist/sde/errors.d.ts.map +1 -1
- package/dist/sde/index.d.mts +7 -6
- package/dist/sde/index.d.ts +7 -6
- package/dist/sde/index.d.ts.map +1 -1
- package/dist/sde/index.js +31 -21
- package/dist/sde/index.js.map +1 -1
- package/dist/sde/index.mjs +26 -16
- package/dist/sde/index.mjs.map +1 -1
- package/dist/sde/ingestion/SdeDatabaseBuilder.d.ts +11 -0
- package/dist/sde/ingestion/SdeDatabaseBuilder.d.ts.map +1 -1
- package/dist/sde/ingestion/SdeDownloader.d.ts +2 -2
- package/dist/sde/ingestion/SdeDownloader.d.ts.map +1 -1
- package/dist/sde/memory.d.mts +5 -5
- package/dist/sde/memory.d.ts +5 -5
- package/dist/sde/memory.d.ts.map +1 -1
- package/dist/sde/memory.js +2 -2
- package/dist/sde/memory.mjs +1 -1
- package/dist/sde/{IStaticDataProvider.d.mts → ports/IStaticDataProvider.d.mts} +9 -2
- package/dist/sde/{IStaticDataProvider.d.ts → ports/IStaticDataProvider.d.ts} +9 -2
- package/dist/sde/ports/IStaticDataProvider.d.ts.map +1 -0
- package/dist/sde/{MemorySdeProvider.d.mts → providers/memory/MemorySdeProvider.d.mts} +3 -3
- package/dist/sde/{MemorySdeProvider.d.ts → providers/memory/MemorySdeProvider.d.ts} +3 -3
- package/dist/sde/providers/memory/MemorySdeProvider.d.ts.map +1 -0
- package/dist/sde/providers/order.d.ts +10 -0
- package/dist/sde/providers/order.d.ts.map +1 -0
- package/dist/sde/{SdeDataProvider.d.mts → providers/yaml/SdeDataProvider.d.mts} +14 -5
- package/dist/sde/{SdeDataProvider.d.ts → providers/yaml/SdeDataProvider.d.ts} +14 -5
- package/dist/sde/providers/yaml/SdeDataProvider.d.ts.map +1 -0
- package/dist/sde/{SdeTestDataFactory.d.mts → testing/SdeTestDataFactory.d.mts} +3 -3
- package/dist/sde/{SdeTestDataFactory.d.ts → testing/SdeTestDataFactory.d.ts} +3 -3
- package/dist/sde/testing/SdeTestDataFactory.d.ts.map +1 -0
- package/dist/sde/version.d.mts +1 -1
- package/dist/sde/version.d.ts +1 -1
- package/dist/sde/version.d.ts.map +1 -1
- package/dist/testing/TestDataFactory.d.mts +21 -3
- package/dist/testing/TestDataFactory.d.ts +21 -3
- package/dist/testing/TestDataFactory.d.ts.map +1 -1
- package/dist/testing/index.d.mts +1 -0
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +157 -10
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/index.mjs +155 -8
- package/dist/testing/index.mjs.map +1 -1
- package/dist/testing/mockTransport.d.mts +87 -0
- package/dist/testing/mockTransport.d.ts +87 -0
- package/dist/testing/mockTransport.d.ts.map +1 -0
- package/dist/types/generated/esi-spec.generated.d.mts +230 -10
- package/dist/types/generated/esi-spec.generated.d.ts +230 -10
- package/dist/types/generated/esi-spec.generated.d.ts.map +1 -1
- package/dist/types/military-campaigns.d.mts +4 -1
- package/dist/types/military-campaigns.d.ts +4 -1
- package/dist/types/military-campaigns.d.ts.map +1 -1
- package/dist/types/skyhooks.d.mts +2 -1
- package/dist/types/skyhooks.d.ts +2 -1
- package/dist/types/skyhooks.d.ts.map +1 -1
- package/package.json +167 -131
- package/dist/chunk-3ZI5A37L.mjs.map +0 -1
- package/dist/chunk-4W66ONJI.mjs +0 -413
- package/dist/chunk-4W66ONJI.mjs.map +0 -1
- package/dist/chunk-BBSHKVPI.js.map +0 -1
- package/dist/chunk-MSJHIJXA.js.map +0 -1
- package/dist/chunk-N3B65VLB.js +0 -413
- package/dist/chunk-N3B65VLB.js.map +0 -1
- package/dist/chunk-R7D7M2LQ.mjs.map +0 -1
- package/dist/chunk-SAQBXXTM.js.map +0 -1
- package/dist/chunk-VZS32UQ3.mjs.map +0 -1
- package/dist/config/jest/globalSetup.d.ts +0 -2
- package/dist/config/jest/globalSetup.d.ts.map +0 -1
- package/dist/config/jest/globalTeardown.d.ts +0 -2
- package/dist/config/jest/globalTeardown.d.ts.map +0 -1
- package/dist/config/jest/jest.setup.d.ts +0 -3
- package/dist/config/jest/jest.setup.d.ts.map +0 -1
- package/dist/core/ClientRegistry.d.mts +0 -45
- package/dist/core/ClientRegistry.d.ts +0 -45
- package/dist/core/ClientRegistry.d.ts.map +0 -1
- package/dist/core/logger/logger.d.ts +0 -12
- package/dist/core/logger/logger.d.ts.map +0 -1
- package/dist/sde/IStaticDataProvider.d.ts.map +0 -1
- package/dist/sde/MemorySdeProvider.d.ts.map +0 -1
- package/dist/sde/SdeDataProvider.d.ts.map +0 -1
- package/dist/sde/SdeTestDataFactory.d.ts.map +0 -1
- package/dist/sde/schemas.d.ts +0 -1121
- package/dist/sde/schemas.d.ts.map +0 -1
- package/dist/sde/types.d.mts +0 -1039
- package/dist/sde/types.d.ts +0 -1039
- package/dist/sde/types.d.ts.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/ESI.ts/ESI.ts/dist/chunk-NKFIHGC6.js","../src/core/middleware/Middleware.ts","../src/core/util/validation.ts","../src/core/ApiClient.ts","../src/core/logger/DefaultLogger.ts","../src/core/logger/redactLog.ts","../src/core/logger/loggerUtil.ts","../src/core/logger/resolveLogger.ts","../src/core/logger/clientLog.ts","../src/core/circuitBreaker/CircuitBreaker.ts","../src/core/util/retry.ts","../src/core/util/sleep.ts","../src/core/RetryStrategy.ts","../src/core/util/callerIdentity.ts","../src/core/cache/cacheKey.ts","../src/core/util/stringUtil.ts","../src/core/endpoints/esi-cache-ttls.generated.ts","../src/core/constants.ts","../src/core/requestPipeline/headers.ts","../src/core/requestPipeline/cachePolicy.ts","../src/core/requestPipeline/statusHandling.ts","../src/core/requestPipeline/dependencies.ts","../src/core/pagination/PaginationHandler.ts","../src/core/requestPipeline/paginationOrchestration.ts","../src/core/requestPipeline/middlewareBridge.ts","../src/core/util/headersUtil.ts","../src/core/requestPipeline/fetchExecution.ts","../src/core/ApiRequestHandler.ts","../src/core/pagination/AsyncPaginationIterator.ts","../src/core/cache/ETagCacheManager.ts","../src/core/endpoints/esi-rate-limit-groups.generated.ts","../src/core/rateLimiter/RateLimiter.ts","../src/core/RequestDeduplicator.ts","../src/core/configureApiClient.ts"],"names":["logInfo","logWarn","logDebug","resolveCache","logError","resolveRateLimiter","resolveCircuitBreaker","data","bucket"],"mappings":"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,EAAA;AACF,EAAA;AACH,EAAA;AACK,EAAA;AACA,EAAA;AACA,EAAA;AACN,EAAA;AACjD,EAAA;AACA,EAAA;AACuD,EAAA;AACN,EAAA;AACC,EAAA;AACI,EAAA;AACtD,EAAA;AACuD,EAAA;AACF,EAAA;AACC,EAAA;AACD,EAAA;AACA,EAAA;AACC,EAAA;AACD,EAAA;AACC,EAAA;AACA,EAAA;AACC,EAAA;AACvD,EAAA;AACA,EAAA;AACA,EAAA;AACoD,EAAA;AACC,EAAA;AACD,EAAA;AACH,EAAA;AACA,EAAA;AACjD,EAAA;AACkD,EAAA;AAClD,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACqD,EAAA;AACC,EAAA;AACtD,EAAA;AACA,EAAA;AACoD,EAAA;AACE,EAAA;AACA,EAAA;AACH,EAAA;AACC,EAAA;AACE,EAAA;AACJ,EAAA;AACE,EAAA;AACG,EAAA;AACN,EAAA;AACC,EAAA;AACA,EAAA;AACG,EAAA;AACF,EAAA;AACH,EAAA;AACR,EAAA;AACa,EAAA;AACV,EAAA;AACE,EAAA;AACC,EAAA;AACE,EAAA;AACH,EAAA;AACM,EAAA;AACH,EAAA;AACH,EAAA;AACC,EAAA;AACG,EAAA;AACN,EAAA;AACY,EAAA;AACN,EAAA;AACC,EAAA;AACE,EAAA;AACjB,EAAA;AACc,EAAA;AACI,EAAA;AACrD,EAAA;AACiD,EAAA;AACE,EAAA;AACE,EAAA;AACE,EAAA;AACvD,EAAA;AAC8C,EAAA;AACO,EAAA;AACF,EAAA;AACF,EAAA;AACJ,EAAA;AACC,EAAA;AACG,EAAA;AACE,EAAA;AACN,EAAA;AACK,EAAA;AACG,EAAA;AACD,EAAA;AACtD;A9BotEyD;AACA;A+Br3EP;AACE,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AAC3C,EAAA;AACT;AAO4E;AAC/C,EAAA;AACsB,EAAA;AACX,EAAA;AACP,EAAA;AACS,EAAA;AAC1C;AAqC0B;AACmB;AACnC,EAAA;AACD,EAAA;AACS,EAAA;AAClB;AAEiD;AAC9B,EAAA;AACA,EAAA;AACA,EAAA;AA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","file":"/home/runner/work/ESI.ts/ESI.ts/dist/chunk-NKFIHGC6.js","sourcesContent":[null,"export interface RequestContext {\n url: string;\n endpoint: string;\n method: string;\n headers: Record<string, string>;\n body?: unknown;\n}\n\nexport interface ResponseContext {\n url: string;\n endpoint: string;\n method: string;\n status: number;\n headers: Record<string, string>;\n body: unknown;\n durationMs: number;\n fromCache: boolean;\n}\n\nexport type RequestInterceptor = (\n context: RequestContext,\n) => RequestContext | Promise<RequestContext>;\n\nexport type ResponseInterceptor = (\n context: ResponseContext,\n) => ResponseContext | Promise<ResponseContext>;\n\nexport class MiddlewareManager {\n private requestInterceptors: RequestInterceptor[] = [];\n private responseInterceptors: ResponseInterceptor[] = [];\n\n addRequestInterceptor(fn: RequestInterceptor): () => void {\n this.requestInterceptors.push(fn);\n return () => {\n this.requestInterceptors = this.requestInterceptors.filter(\n (i) => i !== fn,\n );\n };\n }\n\n addResponseInterceptor(fn: ResponseInterceptor): () => void {\n this.responseInterceptors.push(fn);\n return () => {\n this.responseInterceptors = this.responseInterceptors.filter(\n (i) => i !== fn,\n );\n };\n }\n\n async applyRequestInterceptors(\n context: RequestContext,\n ): Promise<RequestContext> {\n let ctx = context;\n for (const interceptor of this.requestInterceptors) {\n ctx = await interceptor(ctx);\n }\n return ctx;\n }\n\n async applyResponseInterceptors(\n context: ResponseContext,\n ): Promise<ResponseContext> {\n let ctx = context;\n for (const interceptor of this.responseInterceptors) {\n ctx = await interceptor(ctx);\n }\n return ctx;\n }\n\n hasInterceptors(): boolean {\n return (\n this.requestInterceptors.length > 0 ||\n this.responseInterceptors.length > 0\n );\n }\n\n clear(): void {\n this.requestInterceptors = [];\n this.responseInterceptors = [];\n }\n}\n","import { EsiConfigurationError } from './error';\n\nconst UNSAFE_PATH_CHARS = /[/\\\\?#@!$&'()*+,;=<>{}|^`]/;\n\nexport function validatePathParam(paramName: string, value: unknown): string {\n const str =\n value === null || value === undefined ? '' : `${value as string | number}`;\n\n // Checked on the string form: an empty array stringifies to '' too, and\n // would otherwise leave an empty path segment.\n if (str === '') {\n throw new EsiConfigurationError(\n 'VALIDATION_ERROR',\n `Path parameter '${paramName}' must not be empty`,\n );\n }\n\n if (UNSAFE_PATH_CHARS.test(str)) {\n throw new EsiConfigurationError(\n 'VALIDATION_ERROR',\n `Path parameter '${paramName}' contains invalid characters`,\n );\n }\n\n // A lone `.` or `..` survives encodeURIComponent and is collapsed by URL\n // resolution, so `characters/../assets/` would reach `/assets/`.\n if (str === '.' || str === '..') {\n throw new EsiConfigurationError(\n 'VALIDATION_ERROR',\n `Path parameter '${paramName}' must not be a dot segment`,\n );\n }\n\n if (typeof value === 'number' && !Number.isFinite(value)) {\n throw new EsiConfigurationError(\n 'VALIDATION_ERROR',\n `Path parameter '${paramName}' must be a finite number`,\n );\n }\n\n return str;\n}\n\nconst ALLOWED_ESI_HOSTS = ['esi.evetech.net'];\n\nexport function validateBaseUrl(\n url: string,\n allowCustomHost?: boolean,\n): string {\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n throw new EsiConfigurationError(\n 'VALIDATION_ERROR',\n `Invalid base URL: ${url}`,\n );\n }\n\n if (parsed.protocol !== 'https:') {\n throw new EsiConfigurationError(\n 'VALIDATION_ERROR',\n 'Base URL must use HTTPS protocol',\n );\n }\n\n if (!allowCustomHost && !ALLOWED_ESI_HOSTS.includes(parsed.hostname)) {\n throw new EsiConfigurationError(\n 'VALIDATION_ERROR',\n `Base URL host '${parsed.hostname}' is not in the allowlist. ` +\n `Allowed hosts: ${ALLOWED_ESI_HOSTS.join(', ')}. ` +\n `Set unsafeAllowCustomHost to bypass this check.`,\n );\n }\n\n return url.replace(/\\/$/, '');\n}\n\nexport function validateQueryParam(paramName: string, value: unknown): string {\n if (value === null || value === undefined) {\n throw new EsiConfigurationError(\n 'VALIDATION_ERROR',\n `Query parameter '${paramName}' must not be null or undefined`,\n );\n }\n\n const str = `${value as string | number}`;\n\n if (typeof value === 'number' && !Number.isFinite(value)) {\n throw new EsiConfigurationError(\n 'VALIDATION_ERROR',\n `Query parameter '${paramName}' must be a finite number`,\n );\n }\n\n if (str.length > 2000) {\n throw new EsiConfigurationError(\n 'VALIDATION_ERROR',\n `Query parameter '${paramName}' exceeds maximum length`,\n );\n }\n\n return str;\n}\n\n/** Printable ASCII and tab: a header value fetch accepts on every runtime. */\nconst HEADER_VALUE = /^[\\t\\x20-\\x7e]+$/;\n\n/**\n * A client option that becomes a request header (`tenant`, `userAgent`). fetch\n * rejects a value holding a control character on every request, so the value\n * is refused once, where it was configured.\n */\nexport function validateHeaderOption(option: string, value: string): string {\n if (!HEADER_VALUE.test(value)) {\n throw new EsiConfigurationError(\n 'VALIDATION_ERROR',\n `${option} must be printable ASCII with no line breaks or control characters`,\n );\n }\n return value;\n}\n","import {\n MiddlewareManager,\n RequestInterceptor,\n ResponseInterceptor,\n} from './middleware/Middleware';\nimport { ICache } from './cache/ICache';\nimport { IRateLimiter } from './rateLimiter/IRateLimiter';\nimport { ICircuitBreaker } from './circuitBreaker/ICircuitBreaker';\nimport { IDeduplicator } from './IDeduplicator';\nimport { IRetryStrategy } from './IRetryStrategy';\nimport { RetryConfig } from './util/retry';\nimport type { ILogger } from './logger/ILogger';\nimport { validateHeaderOption } from './util/validation';\n\nexport type EsiDatasource = 'tranquility' | 'singularity';\n\nexport type TokenProvider = () => Promise<string>;\n\nexport type FetchLike = (\n input: RequestInfo | URL,\n init?: RequestInit,\n) => Promise<Response>;\n\nexport class ApiClient {\n private datasource?: EsiDatasource | undefined;\n private tokenProvider?: TokenProvider | undefined;\n private refreshInFlight?: Promise<string> | undefined;\n private middleware: MiddlewareManager = new MiddlewareManager();\n private cache: ICache | null = null;\n private rateLimiter: IRateLimiter | null = null;\n private circuitBreaker: ICircuitBreaker | null = null;\n private timeout: number = 30_000;\n private deduplicator: IDeduplicator | null = null;\n private retryConfig: RetryConfig | null = null;\n private retryStrategy: IRetryStrategy | null = null;\n private validateResponse: boolean = true;\n private validateRequest: boolean = false;\n private language?: string | undefined;\n private tenant?: string | undefined;\n private userAgent?: string | undefined;\n private compatibilityDate?: string | undefined;\n private fetchFn: FetchLike | null = null;\n private logger: ILogger | null = null;\n\n constructor(\n private clientId: string,\n private link: string,\n private accessToken?: string,\n ) {\n this.link = this.link.replace(/\\/$/, '');\n }\n\n getTimeout(): number {\n return this.timeout;\n }\n\n setTimeout(timeout: number): void {\n this.timeout = timeout;\n }\n\n getCache(): ICache | null {\n return this.cache;\n }\n\n setCache(cache: ICache | null): void {\n this.cache = cache;\n }\n\n getRateLimiter(): IRateLimiter | null {\n return this.rateLimiter;\n }\n\n setRateLimiter(limiter: IRateLimiter | null): void {\n this.rateLimiter = limiter;\n }\n\n getCircuitBreaker(): ICircuitBreaker | null {\n return this.circuitBreaker;\n }\n\n setCircuitBreaker(cb: ICircuitBreaker | null): void {\n this.circuitBreaker = cb;\n }\n\n getDeduplicator(): IDeduplicator | null {\n return this.deduplicator;\n }\n\n setDeduplicator(dedup: IDeduplicator | null): void {\n this.deduplicator = dedup;\n }\n\n getRetryConfig(): RetryConfig | null {\n return this.retryConfig;\n }\n\n setRetryConfig(config: RetryConfig | null): void {\n this.retryConfig = config;\n }\n\n getRetryStrategy(): IRetryStrategy | null {\n return this.retryStrategy;\n }\n\n setRetryStrategy(strategy: IRetryStrategy | null): void {\n this.retryStrategy = strategy;\n }\n\n getValidateResponse(): boolean {\n return this.validateResponse;\n }\n\n setValidateResponse(validate: boolean): void {\n this.validateResponse = validate;\n }\n\n getValidateRequest(): boolean {\n return this.validateRequest;\n }\n\n setValidateRequest(validate: boolean): void {\n this.validateRequest = validate;\n }\n\n getFetch(): FetchLike {\n return this.fetchFn ?? globalThis.fetch;\n }\n\n setFetch(fn: FetchLike): void {\n this.fetchFn = fn;\n }\n\n getMiddleware(): MiddlewareManager {\n return this.middleware;\n }\n\n addRequestInterceptor(fn: RequestInterceptor): () => void {\n return this.middleware.addRequestInterceptor(fn);\n }\n\n addResponseInterceptor(fn: ResponseInterceptor): () => void {\n return this.middleware.addResponseInterceptor(fn);\n }\n\n getAuthorizationHeader(): string | undefined {\n return this.accessToken ? `Bearer ${this.accessToken}` : undefined;\n }\n\n getLink(): string {\n return this.link;\n }\n\n getClientId(): string {\n return this.clientId;\n }\n\n getDatasource(): EsiDatasource | undefined {\n return this.datasource;\n }\n\n setDatasource(datasource: EsiDatasource | undefined): void {\n this.datasource = datasource;\n }\n\n getLanguage(): string | undefined {\n return this.language;\n }\n\n setLanguage(language: string | undefined): void {\n this.language = language;\n }\n\n /** The `X-Tenant` every request names, or undefined for ESI's default. */\n getTenant(): string | undefined {\n return this.tenant;\n }\n\n /** @throws VALIDATION_ERROR when the value is not printable ASCII. */\n setTenant(tenant: string | undefined): void {\n this.tenant =\n tenant === undefined ? undefined : validateHeaderOption('tenant', tenant);\n }\n\n /** The application's own user agent, sent ahead of the library's. */\n getUserAgent(): string | undefined {\n return this.userAgent;\n }\n\n /** @throws VALIDATION_ERROR when the value is not printable ASCII. */\n setUserAgent(userAgent: string | undefined): void {\n this.userAgent =\n userAgent === undefined\n ? undefined\n : validateHeaderOption('userAgent', userAgent);\n }\n\n getCompatibilityDate(): string | undefined {\n return this.compatibilityDate;\n }\n\n setCompatibilityDate(date: string | undefined): void {\n this.compatibilityDate = date;\n }\n\n setAccessToken(token: string): void {\n this.accessToken = token;\n }\n\n setTokenProvider(provider: TokenProvider | undefined): void {\n this.tokenProvider = provider;\n }\n\n getLogger(): ILogger | null {\n return this.logger;\n }\n\n setLogger(logger: ILogger | null): void {\n this.logger = logger;\n }\n\n hasTokenProvider(): boolean {\n return this.tokenProvider !== undefined;\n }\n\n toJSON(): Record<string, unknown> {\n return {\n link: this.link,\n datasource: this.datasource,\n timeout: this.timeout,\n hasCache: this.cache !== null,\n hasRateLimiter: this.rateLimiter !== null,\n hasCircuitBreaker: this.circuitBreaker !== null,\n hasDeduplicator: this.deduplicator !== null,\n hasTokenProvider: this.tokenProvider !== undefined,\n validateResponse: this.validateResponse,\n validateRequest: this.validateRequest,\n language: this.language,\n compatibilityDate: this.compatibilityDate,\n };\n }\n\n async refreshToken(): Promise<string> {\n if (!this.tokenProvider) {\n throw new Error('No token provider configured');\n }\n\n if (this.refreshInFlight) {\n return this.refreshInFlight;\n }\n\n this.refreshInFlight = this.tokenProvider().then(\n (token) => {\n this.accessToken = token;\n this.refreshInFlight = undefined;\n return token;\n },\n (err) => {\n this.refreshInFlight = undefined;\n throw err;\n },\n );\n\n return this.refreshInFlight;\n }\n}\n","import pino from 'pino';\nimport type { ILogger, LogContext, LoggerLevel } from './ILogger';\n\n/**\n * The default `ILogger` implementation — a thin wrapper around pino.\n *\n * Level resolution (highest precedence first):\n * 1. `level` passed to `createDefaultLogger` (e.g. from\n * `EsiClientConfig.logLevel`).\n * 2. The `ESI_LOG_LEVEL` environment variable.\n * 3. `'warn'` (library default — quiet unless the app opts in).\n *\n * The logger writes to `process.stdout` and can be swapped entirely via\n * `EsiClientConfig.logger` or the global `setLogger()` fallback.\n */\nexport type LogLevel = 'fatal' | 'error' | 'warn' | 'info' | 'debug' | 'trace';\n\n/**\n * An `ILogger` whose methods are plain functions that ignore `this`, so they\n * can be copied onto another object. `toPinoLogger` builds one.\n */\ntype DetachedLogger = {\n [L in LoggerLevel]: (message: string, context?: LogContext) => void;\n} & { isLevelEnabled?: (level: LoggerLevel) => boolean };\n\nexport function createDefaultLogger(level?: string): ILogger {\n return buildDefaultLogger(level);\n}\n\nfunction buildDefaultLogger(level?: string): DetachedLogger {\n const resolved = level || process.env.ESI_LOG_LEVEL || 'warn';\n return adaptPino(pino({ level: resolved }));\n}\n\n/**\n * Adapt any pino-compatible logger (pino, pino-http child, custom sink) to the\n * `ILogger` contract. Context objects are spread onto the log event so they\n * appear as structured fields rather than being stringified into the message.\n */\nexport function toPinoLogger(p: {\n fatal: (...a: unknown[]) => void;\n error: (...a: unknown[]) => void;\n warn: (...a: unknown[]) => void;\n info: (...a: unknown[]) => void;\n debug: (...a: unknown[]) => void;\n trace: (...a: unknown[]) => void;\n isLevelEnabled?: ((level: string) => boolean) | undefined;\n levelVal?: number | undefined;\n levels?: { values: Record<string, number> } | undefined;\n}): ILogger {\n return adaptPino(p);\n}\n\nfunction adaptPino(p: Parameters<typeof toPinoLogger>[0]): DetachedLogger {\n // Call through the sink object so pino methods keep their `this` binding.\n const emit =\n (level: LogLevel) =>\n (message: string, context?: LogContext): void =>\n context ? p[level](context, message) : p[level](message);\n const logger: DetachedLogger = {\n fatal: emit('fatal'),\n error: emit('error'),\n warn: emit('warn'),\n info: emit('info'),\n debug: emit('debug'),\n trace: emit('trace'),\n };\n const values = p.levels?.values;\n if (values && typeof p.levelVal === 'number') {\n // pino's own isLevelEnabled costs more than the line it would skip; the\n // numeric comparison reads the current level, so a later change applies.\n logger.isLevelEnabled = (level) =>\n (values[level] ?? Infinity) >= (p.levelVal ?? -Infinity);\n } else if (typeof p.isLevelEnabled === 'function') {\n logger.isLevelEnabled = (level) => p.isLevelEnabled?.(level) ?? true;\n }\n return logger;\n}\n\nconst LEVELS: readonly LoggerLevel[] = [\n 'fatal',\n 'error',\n 'warn',\n 'info',\n 'debug',\n 'trace',\n];\n\n/**\n * An `ILogger` that builds its pino logger on first use: the first call to a\n * level method or to `isLevelEnabled`. Nothing is constructed when the module\n * loads, so importing the package starts no pino instance (CHARTER ARCH-06)\n * and `ESI_LOG_LEVEL` is read when the first line is logged, not at import.\n *\n * On that first call the object replaces its own methods with the built\n * logger's, so every later call goes straight to pino with no extra hop.\n * The object's identity never changes, so it can be held and compared.\n */\nexport function createLazyDefaultLogger(): ILogger {\n const lazy = {} as DetachedLogger;\n // Cached so a caller that saved a method before first use (`const warn =\n // logger.warn`) reaches the same pino instance on every call.\n let built: DetachedLogger | undefined;\n const materialise = (): DetachedLogger => {\n if (built) return built;\n const real = buildDefaultLogger();\n built = real;\n for (const level of LEVELS) lazy[level] = real[level];\n lazy.isLevelEnabled = real.isLevelEnabled ?? ((): boolean => true);\n return real;\n };\n for (const level of LEVELS) {\n lazy[level] = (message: string, context?: LogContext): void =>\n materialise()[level](message, context);\n }\n lazy.isLevelEnabled = (level: LoggerLevel): boolean =>\n materialise().isLevelEnabled?.(level) ?? true;\n return lazy;\n}\n\n/**\n * Backwards-compatible default instance (level from `ESI_LOG_LEVEL`). Built\n * lazily: the pino logger behind it is constructed on its first use.\n */\nexport const defaultLogger: ILogger = createLazyDefaultLogger();\n","import { SENSITIVE_QUERY_PATTERN, isSensitiveQueryParam } from '../util/error';\nimport type { ILogger, LogContext, LoggerLevel } from './ILogger';\n\n/**\n * One `name=value` pair of a query: the separator before it, the name as\n * written, and the value up to the next separator, whitespace, or character\n * that ends a URL in prose (quotes, brackets, a comma or semicolon).\n */\nconst QUERY_PAIR = /([?&])([^=&#?\\s]+)=([^&#\\s,;\"'`<>()[\\]{}]*)/g;\n\n/** The value `sanitizeUrl` writes for a redacted parameter, URL-encoded. */\nconst REDACTED = '%5BREDACTED%5D';\n\n/**\n * Redact the sensitive query parameters of every URL in a piece of log text,\n * matching the names `sanitizeUrl` redacts. It works on the text itself\n * rather than parsing each URL, so URLs next to punctuation, relative and\n * protocol-relative paths, and URLs `new URL` rejects are all covered, and\n * everything else in the line keeps its exact wording.\n */\nexport function redactLogText(text: string): string {\n if (!text.includes('?') || !SENSITIVE_QUERY_PATTERN.test(text)) return text;\n return text.replace(\n QUERY_PAIR,\n (pair: string, separator: string, name: string, value: string) =>\n value !== '' && isSensitiveQueryParam(name)\n ? `${separator}${name}=${REDACTED}`\n : pair,\n );\n}\n\n/**\n * Redact the log context: every top-level string value (a `url`, an\n * `endpoint`, a path) passes through the same URL redaction as the message.\n * Returns `undefined` for a missing or empty context so a logger never\n * receives `{}`.\n */\nexport function redactLogContext(\n context: LogContext | undefined,\n): LogContext | undefined {\n if (!context) return undefined;\n const keys = Object.keys(context);\n if (keys.length === 0) return undefined;\n for (const key of keys) {\n if (mayCarrySecret(context[key])) return redactValues(context, keys);\n }\n return context;\n}\n\n/** The prefilter `redactLogText` applies, inlined for the hot path. */\nfunction mayCarrySecret(value: unknown): value is string {\n return (\n typeof value === 'string' &&\n value.includes('?') &&\n SENSITIVE_QUERY_PATTERN.test(value)\n );\n}\n\nfunction redactValues(\n context: LogContext,\n keys: readonly string[],\n): LogContext {\n const redacted: LogContext = {};\n for (const key of keys) {\n const value = context[key];\n redacted[key] = mayCarrySecret(value) ? redactLogText(value) : value;\n }\n return redacted;\n}\n\n/**\n * A view of a logger that skips disabled levels and redacts each line the\n * way the pipeline's log helpers do, for code that holds a logger directly\n * rather than logging through a client.\n */\nexport function redactingLogger(logger: ILogger): ILogger {\n const at =\n (level: LoggerLevel) =>\n (message: string, context?: LogContext): void => {\n if (logger.isLevelEnabled?.(level) === false) return;\n const safe = redactLogContext(context);\n if (safe === undefined) logger[level](redactLogText(message));\n else logger[level](redactLogText(message), safe);\n };\n return {\n fatal: at('fatal'),\n error: at('error'),\n warn: at('warn'),\n info: at('info'),\n debug: at('debug'),\n trace: at('trace'),\n };\n}\n","import type { ILogger, LogContext, LoggerLevel } from './ILogger';\nimport { defaultLogger } from './DefaultLogger';\nimport { redactLogContext, redactLogText } from './redactLog';\n\nexport type { ILogger, LogContext } from './ILogger';\n\n/**\n * Global logger fallback.\n *\n * The request pipeline reads the *per-client* logger first and only falls\n * back to this global when a client has none (e.g. the raw factory\n * `EsiApiFactory` clients, or unit tests that call pipeline modules\n * directly). Production apps can rely on the per-client logger from\n * `EsiClientConfig.logger`; this global exists for backwards compatibility\n * and for apps that want one shared logger across all clients.\n */\nlet activeLogger: ILogger = defaultLogger;\n\nexport const setLogger = (customLogger: ILogger): void => {\n activeLogger = customLogger;\n};\n\nexport const getLogger = (): ILogger => activeLogger;\n\nconst emit =\n (level: LoggerLevel) =>\n (message: string, context?: LogContext): void => {\n if (activeLogger.isLevelEnabled?.(level) === false) return;\n activeLogger[level](redactLogText(message), redactLogContext(context));\n };\n\nexport const logFatal: (message: string, context?: LogContext) => void =\n emit('fatal');\n\nexport const logError: (message: string, context?: LogContext) => void =\n emit('error');\n\nexport const logWarn: (message: string, context?: LogContext) => void =\n emit('warn');\n\nexport const logInfo: (message: string, context?: LogContext) => void =\n emit('info');\n\nexport const logDebug: (message: string, context?: LogContext) => void =\n emit('debug');\n\nexport const logTrace: (message: string, context?: LogContext) => void =\n emit('trace');\n","import type { ILogger } from './ILogger';\nimport { getLogger } from './loggerUtil';\n\n/**\n * Resolve the logger for a given API client: per-client logger first, then\n * the global one — a logger installed with `setLogger`, or the built-in pino\n * default when none was.\n */\nexport function resolveLogger(\n client: { getLogger?: () => ILogger | null } | null | undefined,\n): ILogger {\n return client?.getLogger?.() ?? getLogger();\n}\n","import type { ApiClient } from '../ApiClient';\nimport type { LogContext, LoggerLevel as Level } from './ILogger';\nimport { redactLogContext, redactLogText } from './redactLog';\nimport { resolveLogger } from './resolveLogger';\n\ntype Emit = (\n client: ApiClient | null | undefined,\n message: string,\n context?: LogContext,\n) => void;\n\n/**\n * The logger boundary for the pipeline: every line resolves the per-client\n * logger and has the sensitive query parameters of any URL in its message or\n * top-level string context values redacted through `sanitizeUrl`, so no call\n * site has to remember to sanitise.\n */\nconst emit =\n (level: Level): Emit =>\n (client, message, context) => {\n const logger = resolveLogger(client);\n if (logger.isLevelEnabled?.(level) === false) return;\n logger[level](redactLogText(message), redactLogContext(context));\n };\n\nexport const logFatal: Emit = emit('fatal');\nexport const logError: Emit = emit('error');\nexport const logWarn: Emit = emit('warn');\nexport const logInfo: Emit = emit('info');\nexport const logDebug: Emit = emit('debug');\nexport const logTrace: Emit = emit('trace');\n","import { ApiClient } from '../ApiClient';\nimport { logWarn, logInfo } from '../logger/clientLog';\nimport { ICircuitBreaker } from './ICircuitBreaker';\nimport { CircuitOpenError } from '../util/error';\n\nexport { CircuitOpenError };\n\nexport type CircuitState = 'closed' | 'open' | 'half-open';\n\nexport interface CircuitBreakerConfig {\n failureThreshold?: number | undefined;\n resetTimeoutMs?: number | undefined;\n halfOpenMaxAttempts?: number | undefined;\n staleThresholdMs?: number | undefined;\n /** 'resolved' (default, current behavior) | 'template' (group by endpoint template path) */\n keyStrategy?: 'resolved' | 'template' | undefined;\n /** Interval in ms for automatic cleanup of stale circuits. 0 or undefined disables. Recommended value: staleThresholdMs. */\n cleanupIntervalMs?: number | undefined;\n}\n\ninterface CircuitRecord {\n state: CircuitState;\n failures: number;\n lastFailureTime: number;\n halfOpenAttempts: number;\n}\n\nexport class CircuitBreaker implements ICircuitBreaker {\n private circuits: Map<string, CircuitRecord> = new Map();\n private readonly failureThreshold: number;\n private readonly resetTimeoutMs: number;\n private readonly halfOpenMaxAttempts: number;\n private readonly staleThresholdMs: number;\n private readonly keyStrategy: 'resolved' | 'template';\n private cleanupTimer: ReturnType<typeof setInterval> | null = null;\n private client: ApiClient | null = null;\n\n setClient(client: ApiClient | null): void {\n this.client = client;\n }\n\n constructor(config: CircuitBreakerConfig = {}) {\n this.failureThreshold = config.failureThreshold ?? 5;\n this.resetTimeoutMs = config.resetTimeoutMs ?? 30_000;\n this.halfOpenMaxAttempts = config.halfOpenMaxAttempts ?? 1;\n this.staleThresholdMs = config.staleThresholdMs ?? 3_600_000;\n this.keyStrategy = config.keyStrategy ?? 'resolved';\n\n const cleanupIntervalMs = config.cleanupIntervalMs;\n if (cleanupIntervalMs !== undefined && cleanupIntervalMs > 0) {\n this.startCleanupTimer(cleanupIntervalMs);\n }\n }\n\n private getKey(endpoint: string): string {\n return endpoint.split('?')[0]!;\n }\n\n private getOrCreate(key: string): CircuitRecord {\n let record = this.circuits.get(key);\n if (!record) {\n record = {\n state: 'closed',\n failures: 0,\n lastFailureTime: 0,\n halfOpenAttempts: 0,\n };\n this.circuits.set(key, record);\n }\n return record;\n }\n\n checkCircuit(endpoint: string): void {\n const key = this.getKey(endpoint);\n const record = this.getOrCreate(key);\n\n if (record.state === 'closed') return;\n\n if (record.state === 'open') {\n const elapsed = Date.now() - record.lastFailureTime;\n if (elapsed >= this.resetTimeoutMs) {\n record.state = 'half-open';\n // This call is the first probe, so it counts towards halfOpenMaxAttempts.\n record.halfOpenAttempts = 1;\n logInfo(\n this.client,\n `Circuit half-open for ${key}, allowing probe request`,\n { key },\n );\n return;\n }\n const remainingMs = this.resetTimeoutMs - elapsed;\n throw new CircuitOpenError(key, record.failures, remainingMs);\n }\n\n if (record.state === 'half-open') {\n if (record.halfOpenAttempts >= this.halfOpenMaxAttempts) {\n throw new CircuitOpenError(key, record.failures, this.resetTimeoutMs);\n }\n record.halfOpenAttempts++;\n }\n }\n\n recordSuccess(endpoint: string): void {\n const key = this.getKey(endpoint);\n const record = this.circuits.get(key);\n if (!record) return;\n\n // A success while open comes from a call admitted before the circuit\n // opened. Only a half-open probe may close the circuit.\n if (record.state === 'open') return;\n\n if (record.state === 'half-open') {\n logInfo(this.client, `Circuit closed for ${key} after successful probe`, {\n key,\n });\n }\n\n record.state = 'closed';\n record.failures = 0;\n record.halfOpenAttempts = 0;\n }\n\n recordFailure(endpoint: string, statusCode: number): void {\n // statusCode 0 = network/timeout; 420/429 = rate limited; >=500 = server errors\n if (\n statusCode !== 0 &&\n statusCode < 500 &&\n statusCode !== 420 &&\n statusCode !== 429\n ) {\n return;\n }\n\n const key = this.getKey(endpoint);\n const record = this.getOrCreate(key);\n\n record.failures++;\n record.lastFailureTime = Date.now();\n\n if (record.state === 'half-open') {\n record.state = 'open';\n logWarn(\n this.client,\n `Circuit re-opened for ${key} after failed probe (${record.failures} failures)`,\n { key, failures: record.failures },\n );\n return;\n }\n\n if (record.failures >= this.failureThreshold) {\n record.state = 'open';\n logWarn(\n this.client,\n `Circuit opened for ${key} after ${record.failures} consecutive failures`,\n { key, failures: record.failures },\n );\n }\n }\n\n getState(endpoint: string): CircuitState {\n const key = this.getKey(endpoint);\n const record = this.circuits.get(key);\n if (!record) return 'closed';\n\n if (record.state === 'open') {\n const elapsed = Date.now() - record.lastFailureTime;\n if (elapsed >= this.resetTimeoutMs) {\n return 'half-open';\n }\n }\n\n return record.state;\n }\n\n getStats(): {\n totalCircuits: number;\n openCircuits: number;\n circuits: Record<string, { state: CircuitState; failures: number }>;\n } {\n const circuits: Record<string, { state: CircuitState; failures: number }> =\n {};\n let openCircuits = 0;\n\n for (const [key, record] of this.circuits.entries()) {\n const state = this.getState(key);\n // eslint-disable-next-line security/detect-object-injection\n circuits[key] = { state, failures: record.failures };\n if (state === 'open') openCircuits++;\n }\n\n return {\n totalCircuits: this.circuits.size,\n openCircuits,\n circuits,\n };\n }\n\n reset(endpoint?: string): void {\n if (endpoint) {\n this.circuits.delete(this.getKey(endpoint));\n } else {\n this.circuits.clear();\n }\n }\n\n cleanup(): number {\n const now = Date.now();\n let cleaned = 0;\n for (const [key, record] of this.circuits.entries()) {\n if (\n record.state === 'closed' &&\n record.failures === 0 &&\n record.lastFailureTime > 0 &&\n now - record.lastFailureTime > this.staleThresholdMs\n ) {\n this.circuits.delete(key);\n cleaned++;\n }\n }\n if (cleaned > 0) {\n logInfo(\n this.client,\n `Circuit breaker cleanup: removed ${cleaned} stale circuits`,\n {\n cleaned,\n },\n );\n }\n return cleaned;\n }\n\n getKeyStrategy(): 'resolved' | 'template' {\n return this.keyStrategy;\n }\n\n destroy(): void {\n if (this.cleanupTimer) {\n clearInterval(this.cleanupTimer);\n this.cleanupTimer = null;\n }\n this.circuits.clear();\n }\n\n shutdown(): void {\n this.destroy();\n }\n\n private startCleanupTimer(intervalMs: number): void {\n this.cleanupTimer = setInterval(() => {\n this.cleanup();\n }, intervalMs);\n // Don't prevent process exit (important for test runners and CLI tools)\n this.cleanupTimer.unref();\n }\n}\n","export interface RetryConfig {\n maxRetries?: number | undefined;\n baseDelayMs?: number | undefined;\n maxDelayMs?: number | undefined;\n retryMutations?: boolean | undefined;\n}\n\nexport function retryDelay(\n attempt: number,\n baseMs: number,\n maxMs: number,\n): number {\n // 2^attempt overflows to Infinity from attempt 1024, and 0 × Infinity is NaN.\n const exponential = baseMs === 0 ? 0 : baseMs * Math.pow(2, attempt);\n // eslint-disable-next-line sonarjs/pseudo-random\n const jitter = exponential * (0.75 + Math.random() * 0.5);\n return Math.min(jitter, maxMs);\n}\n","export function sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","import { ApiClient } from './ApiClient';\nimport { EsiError, EsiTokenRefreshError } from './util/error';\nimport { CircuitOpenError } from './circuitBreaker/CircuitBreaker';\nimport { RetryConfig, retryDelay } from './util/retry';\nimport { sleep } from './util/sleep';\nimport { logInfo, logWarn, logError } from './logger/clientLog';\nimport { IRetryStrategy } from './IRetryStrategy';\n\nexport interface RetryContext {\n client?: ApiClient | undefined;\n endpoint: string;\n method: string;\n requiresAuth: boolean;\n refreshToken?: (() => Promise<void>) | undefined;\n /** @deprecated Unused — after refresh the strategy re-enters the main operation loop. */\n retryOperation?: (() => Promise<unknown>) | undefined;\n}\n\nexport class RetryStrategy implements IRetryStrategy {\n private readonly maxRetries: number;\n private readonly baseDelayMs: number;\n private readonly maxDelayMs: number;\n private readonly retryMutations: boolean;\n\n constructor(config?: RetryConfig) {\n this.maxRetries = config?.maxRetries ?? 0;\n this.baseDelayMs = config?.baseDelayMs ?? 1000;\n this.maxDelayMs = config?.maxDelayMs ?? 30000;\n this.retryMutations = config?.retryMutations ?? false;\n }\n\n async execute<T>(\n operation: () => Promise<T>,\n context: RetryContext,\n ): Promise<T> {\n const canRetryMethod = context.method === 'GET' || this.retryMutations;\n\n let lastError: unknown;\n let refreshAttempted = false;\n\n for (let attempt = 0; attempt <= this.maxRetries; attempt++) {\n try {\n return await operation();\n } catch (error: unknown) {\n if (error instanceof CircuitOpenError) {\n throw error;\n }\n\n if (\n !refreshAttempted &&\n error instanceof EsiError &&\n error.statusCode === 401 &&\n context.requiresAuth &&\n context.refreshToken\n ) {\n refreshAttempted = true;\n logInfo(context.client, 'Received 401, attempting token refresh...', {\n endpoint: context.endpoint,\n });\n try {\n await context.refreshToken();\n } catch (refreshError: unknown) {\n if (refreshError instanceof EsiError) {\n throw refreshError;\n }\n const msg =\n refreshError instanceof Error\n ? refreshError.message\n : String(refreshError);\n logError(context.client, `Token refresh failed: ${msg}`, {\n endpoint: context.endpoint,\n });\n throw new EsiTokenRefreshError(refreshError);\n }\n logInfo(context.client, 'Token refreshed, retrying request', {\n endpoint: context.endpoint,\n });\n // Re-enter the loop so post-refresh failures use normal retry/error handling\n // (and are never mislabeled as TOKEN_REFRESH_FAILED).\n attempt--;\n continue;\n }\n\n if (\n error instanceof EsiError &&\n error.retryable &&\n canRetryMethod &&\n attempt < this.maxRetries\n ) {\n const delay = retryDelay(attempt, this.baseDelayMs, this.maxDelayMs);\n logWarn(\n context.client,\n `Request to ${context.endpoint} failed (${error.statusCode}), retrying in ${Math.round(delay)}ms (attempt ${attempt + 1}/${this.maxRetries})`,\n { method: context.method, statusCode: error.statusCode },\n );\n await sleep(delay);\n lastError = error;\n continue;\n }\n\n throw error;\n }\n }\n\n throw lastError;\n }\n}\n","import { createHash } from 'crypto';\n\n/**\n * The identity a bearer token stands for, as a key fragment.\n *\n * An EVE SSO access token is a JWT whose `sub` claim reads\n * `CHARACTER:EVE:<characterId>`. A refresh rotates the token but not the\n * character, so keys that outlive one request (the ETag cache, the in-flight\n * deduplicator) use the character id: the ETags a character has earned\n * survive a refresh, and two tokens for one character share an entry. A\n * token that names no character (an opaque test token, another issuer's\n * token) is keyed by a hash of the header instead, so it still never shares\n * an entry with another token, and the token itself never appears in a key,\n * which reaches logs and statistics.\n *\n * The payload is decoded without verifying the signature, so the character\n * a token names is a claim, not a fact, until ESI has answered a request\n * under it. `src/core/cache/cacheKey.ts` keeps the two apart: a token's\n * claimed identity decides which entry a conditional request revalidates,\n * and only an accepted token is served an entry without ESI's answer.\n * `src/auth/jwt.ts` decodes the same claims for the token manager and throws\n * on malformed input; this reader must not, because a malformed token is a\n * valid (if doomed) identity to key by.\n */\n\nconst CHARACTER_SUBJECT = /^CHARACTER:EVE:(\\d+)$/;\n\n/** The character id an SSO JWT names, or null for anything else. */\nexport function characterIdOfToken(token: string): number | null {\n const parts = token.split('.');\n if (parts.length !== 3 || !parts[1]) return null;\n let claims: unknown;\n try {\n claims = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));\n } catch {\n return null;\n }\n if (typeof claims !== 'object' || claims === null) return null;\n const sub = (claims as { sub?: unknown }).sub;\n const match = typeof sub === 'string' ? CHARACTER_SUBJECT.exec(sub) : null;\n return match ? Number(match[1]) : null;\n}\n\n/** The first 16 hex characters of a SHA-256 of the whole header. */\nexport function hashedIdentity(authorizationHeader: string): string {\n return createHash('sha256')\n .update(authorizationHeader)\n .digest('hex')\n .slice(0, 16);\n}\n\n/**\n * The identity an Authorization header claims: `character:<id>` for an SSO\n * token, else its hash.\n */\nexport function callerIdentity(authorizationHeader: string): string {\n const token = authorizationHeader.startsWith('Bearer ')\n ? authorizationHeader.slice('Bearer '.length)\n : authorizationHeader;\n const characterId = characterIdOfToken(token);\n if (characterId !== null) return `character:${characterId}`;\n return hashedIdentity(authorizationHeader);\n}\n","import { ApiClient } from '../ApiClient';\nimport { callerIdentity, hashedIdentity } from '../util/callerIdentity';\n\n/**\n * Scope a key to the identity whose token the request carries.\n *\n * One client can serve more than one identity: `setAccessToken` and a\n * `tokenProvider` both replace the token in place. A path says nothing about\n * whose data comes back — `characters/{character_id}/online` answers\n * differently for every token that asks it — so any key that outlives a single\n * request has to carry the identity too, or one caller is served another's\n * data.\n *\n * The identity is the character an EVE SSO token names, so a refreshed token\n * keeps the character's entries, or a hash of the header for a token that\n * names none (`callerIdentity`). Either way the bearer token itself never\n * appears in a key: keys reach logs and diagnostics.\n *\n * The character is read from the token without verifying it, so it is a\n * claim until ESI has answered a request under that token. A token ESI has\n * not yet accepted is keyed by its hash wherever an entry would be served\n * without ESI's say (the spec-TTL hit, stale-on-error, joining an in-flight\n * request), and by its claimed character only for the `If-None-Match` it\n * sends: a 304 is ESI accepting the token and the entry in one answer, and a\n * 401 serves nothing. Without this, a forged token naming a character would\n * read that character's cache before ESI ever saw it.\n */\n\ninterface Identity {\n header: string;\n /** `character:<id>` for an SSO token, else the hash. */\n claimed: string;\n hashed: string;\n}\n\n/**\n * Derived once per token per client, not once per request: decoding a JWT on\n * every cache lookup would put base64 and JSON parsing on the hot path, and a\n * client keeps one token for thousands of requests.\n */\nconst identities = new WeakMap<ApiClient, Identity>();\n\n/** Hashes of the headers ESI has answered 2xx or 304 to, per client. */\nconst accepted = new WeakMap<ApiClient, Set<string>>();\n\n/** Refreshes are rare; a flood of rejected tokens costs nothing here. */\nconst ACCEPTED_LIMIT = 64;\n\nfunction identityOf(client: ApiClient, header: string): Identity {\n const known = identities.get(client);\n if (known && known.header === header) return known;\n const identity = {\n header,\n claimed: callerIdentity(header),\n hashed: hashedIdentity(header),\n };\n identities.set(client, identity);\n return identity;\n}\n\n/**\n * Record that ESI accepted the token in `authorizationHeader`, so its claimed\n * character now keys what the client serves without a request. Called with\n * the header the request carried, not the client's current one: a refresh\n * may have replaced the token while the request was in flight.\n */\nexport function markTokenAccepted(\n client: ApiClient,\n authorizationHeader: string,\n): void {\n let set = accepted.get(client);\n if (!set) {\n set = new Set();\n accepted.set(client, set);\n }\n const hashed = hashedIdentity(authorizationHeader);\n if (set.has(hashed)) return;\n if (set.size >= ACCEPTED_LIMIT) {\n set.delete(set.values().next().value as string);\n }\n set.add(hashed);\n}\n\nfunction isAccepted(client: ApiClient, identity: Identity): boolean {\n return accepted.get(client)?.has(identity.hashed) ?? false;\n}\n\nfunction scope(key: string, identity: string | null): string {\n return identity === null ? key : `${identity}:${key}`;\n}\n\n/**\n * The identity that decides what the client serves without asking ESI: the\n * one in `authorizationHeader` when given, else the client's current token.\n */\nfunction trustedIdentity(\n client: ApiClient,\n requiresAuth: boolean,\n authorizationHeader?: string,\n): string | null {\n if (!requiresAuth) return null;\n const header = authorizationHeader ?? client.getAuthorizationHeader();\n if (!header) return null;\n const identity = identityOf(client, header);\n return isAccepted(client, identity) ? identity.claimed : identity.hashed;\n}\n\n/** The identity the token claims, whether or not ESI has accepted it yet. */\nfunction claimedIdentity(\n client: ApiClient,\n requiresAuth: boolean,\n): string | null {\n if (!requiresAuth) return null;\n const header = client.getAuthorizationHeader();\n if (!header) return null;\n return identityOf(client, header).claimed;\n}\n\n/**\n * The ETag cache key for a request: its URL, scoped to the identity ESI has\n * accepted. Reads that serve an entry without a request, and writes of what\n * ESI answered, use this key.\n *\n * Once a request has been sent, `authorizationHeader` is the header it\n * carried: a concurrent request may have replaced the client's token in the\n * meantime, and what ESI answered belongs to the token that asked, not to\n * the one the client holds when the answer arrives.\n */\nexport function buildCacheKey(\n url: string,\n client: ApiClient,\n requiresAuth: boolean = false,\n authorizationHeader?: string,\n): string {\n return scope(url, trustedIdentity(client, requiresAuth, authorizationHeader));\n}\n\n/**\n * The key whose ETag a request sends as `If-None-Match`: the URL scoped to\n * the identity the token claims. Sending the character's ETag under a token\n * ESI has not yet accepted serves nothing by itself; ESI's 304 is what serves\n * the entry, and it comes only to a token ESI accepts.\n */\nexport function buildConditionalCacheKey(\n url: string,\n client: ApiClient,\n requiresAuth: boolean = false,\n): string {\n return scope(url, claimedIdentity(client, requiresAuth));\n}\n\n/**\n * The deduplication key for an in-flight request: its endpoint, scoped to the\n * accepted identity.\n *\n * Coalescing has to draw the same line the cache does. Two concurrent GETs to\n * one authenticated endpoint under tokens for different characters are two\n * different questions, and answering both from one response hands one caller\n * the other identity's data. Under two accepted tokens for one character they\n * are one question, and share the request; a token ESI has not yet accepted\n * joins nothing, since joining is being served without ESI's answer.\n */\nexport function buildDedupeKey(\n endpoint: string,\n client: ApiClient,\n requiresAuth: boolean = false,\n): string {\n return scope(endpoint, trustedIdentity(client, requiresAuth));\n}\n","export function camelToSnake(s: string): string {\n return s.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);\n}\n","// Auto-generated from ESI OpenAPI spec — do not edit manually\n// Compatibility date: 2026-08-18\n// Endpoints with cache TTLs: 146\n\nexport const esiCacheTtls: Record<string, number> = {\n 'GET:alliances': 3600,\n 'GET:alliances/{alliance_id}': 3600,\n 'GET:alliances/{alliance_id}/contacts': 300,\n 'GET:alliances/{alliance_id}/contacts/labels': 300,\n 'GET:alliances/{alliance_id}/corporations': 3600,\n 'GET:characters/{character_id}': 86400,\n 'GET:characters/{character_id}/access-lists': 300,\n 'GET:characters/{character_id}/access-lists/{access_list_id}': 300,\n 'GET:characters/{character_id}/agents_research': 3600,\n 'GET:characters/{character_id}/assets': 3600,\n 'GET:characters/{character_id}/attributes': 120,\n 'GET:characters/{character_id}/blueprints': 3600,\n 'GET:characters/{character_id}/calendar': 5,\n 'GET:characters/{character_id}/calendar/{event_id}': 5,\n 'PUT:characters/{character_id}/calendar/{event_id}': 5,\n 'GET:characters/{character_id}/calendar/{event_id}/attendees': 600,\n 'GET:characters/{character_id}/clones': 120,\n 'GET:characters/{character_id}/contacts': 300,\n 'GET:characters/{character_id}/contacts/labels': 300,\n 'GET:characters/{character_id}/contracts': 300,\n 'GET:characters/{character_id}/contracts/{contract_id}/bids': 300,\n 'GET:characters/{character_id}/contracts/{contract_id}/items': 3600,\n 'GET:characters/{character_id}/corporationhistory': 86400,\n 'GET:characters/{character_id}/cosmetics/skinr': 3600,\n 'GET:characters/{character_id}/cosmetics/skinr/components': 3600,\n 'GET:characters/{character_id}/fatigue': 300,\n 'GET:characters/{character_id}/fittings': 300,\n 'GET:characters/{character_id}/fleet': 60,\n 'GET:characters/{character_id}/freelance-jobs': 60,\n 'GET:characters/{character_id}/freelance-jobs/{job_id}/participation': 60,\n 'GET:characters/{character_id}/implants': 120,\n 'GET:characters/{character_id}/industry/jobs': 300,\n 'GET:characters/{character_id}/killmails/recent': 300,\n 'GET:characters/{character_id}/location': 5,\n 'GET:characters/{character_id}/loyalty/points': 3600,\n 'GET:characters/{character_id}/mail': 30,\n 'GET:characters/{character_id}/mail/{mail_id}': 30,\n 'GET:characters/{character_id}/mail/labels': 30,\n 'GET:characters/{character_id}/mail/lists': 120,\n 'GET:characters/{character_id}/medals': 3600,\n 'GET:characters/{character_id}/mercenary-tactical-operations': 300,\n 'GET:characters/{character_id}/mercenary-tactical-operations/{operation_id}': 300,\n 'GET:characters/{character_id}/military-campaigns/objectives': 60,\n 'GET:characters/{character_id}/military-campaigns/objectives/{objective_id}': 60,\n 'GET:characters/{character_id}/mining': 600,\n 'GET:characters/{character_id}/notifications': 600,\n 'GET:characters/{character_id}/notifications/contacts': 600,\n 'GET:characters/{character_id}/online': 60,\n 'GET:characters/{character_id}/orders': 1200,\n 'GET:characters/{character_id}/orders/history': 3600,\n 'GET:characters/{character_id}/planets': 600,\n 'GET:characters/{character_id}/planets/{planet_id}': 600,\n 'GET:characters/{character_id}/roles': 3600,\n 'GET:characters/{character_id}/search': 3600,\n 'GET:characters/{character_id}/ship': 5,\n 'GET:characters/{character_id}/skillqueue': 60,\n 'GET:characters/{character_id}/skills': 60,\n 'GET:characters/{character_id}/standings': 3600,\n 'GET:characters/{character_id}/structures/mercenary-dens': 3600,\n 'GET:characters/{character_id}/structures/mercenary-dens/{mercenary_den_id}': 3600,\n 'GET:characters/{character_id}/titles': 3600,\n 'GET:characters/{character_id}/wallet': 120,\n 'GET:characters/{character_id}/wallet/journal': 3600,\n 'GET:characters/{character_id}/wallet/transactions': 3600,\n 'POST:characters/affiliation': 3600,\n 'GET:contracts/public/{region_id}': 1800,\n 'GET:contracts/public/bids/{contract_id}': 300,\n 'GET:contracts/public/items/{contract_id}': 3600,\n 'GET:corporation/{corporation_id}/mining/extractions': 1800,\n 'GET:corporation/{corporation_id}/mining/observers': 3600,\n 'GET:corporation/{corporation_id}/mining/observers/{observer_id}': 3600,\n 'GET:corporations/{corporation_id}': 3600,\n 'GET:corporations/{corporation_id}/alliancehistory': 3600,\n 'GET:corporations/{corporation_id}/assets': 3600,\n 'GET:corporations/{corporation_id}/blueprints': 3600,\n 'GET:corporations/{corporation_id}/contacts': 300,\n 'GET:corporations/{corporation_id}/contacts/labels': 300,\n 'GET:corporations/{corporation_id}/containers/logs': 600,\n 'GET:corporations/{corporation_id}/contracts': 300,\n 'GET:corporations/{corporation_id}/contracts/{contract_id}/bids': 3600,\n 'GET:corporations/{corporation_id}/contracts/{contract_id}/items': 3600,\n 'GET:corporations/{corporation_id}/customs_offices': 3600,\n 'GET:corporations/{corporation_id}/divisions': 3600,\n 'GET:corporations/{corporation_id}/facilities': 3600,\n 'GET:corporations/{corporation_id}/icons': 3600,\n 'GET:corporations/{corporation_id}/industry/jobs': 300,\n 'GET:corporations/{corporation_id}/killmails/recent': 300,\n 'GET:corporations/{corporation_id}/medals': 3600,\n 'GET:corporations/{corporation_id}/medals/issued': 3600,\n 'GET:corporations/{corporation_id}/members': 3600,\n 'GET:corporations/{corporation_id}/members/limit': 3600,\n 'GET:corporations/{corporation_id}/members/titles': 3600,\n 'GET:corporations/{corporation_id}/membertracking': 3600,\n 'GET:corporations/{corporation_id}/orders': 1200,\n 'GET:corporations/{corporation_id}/orders/history': 3600,\n 'GET:corporations/{corporation_id}/projects/{project_id}': 60,\n 'GET:corporations/{corporation_id}/projects/{project_id}/contribution/{character_id}': 60,\n 'GET:corporations/{corporation_id}/roles': 3600,\n 'GET:corporations/{corporation_id}/roles/history': 3600,\n 'GET:corporations/{corporation_id}/shareholders': 3600,\n 'GET:corporations/{corporation_id}/standings': 3600,\n 'GET:corporations/{corporation_id}/starbases': 3600,\n 'GET:corporations/{corporation_id}/starbases/{starbase_id}': 3600,\n 'GET:corporations/{corporation_id}/structures': 3600,\n 'GET:corporations/{corporation_id}/structures/skyhooks': 3600,\n 'GET:corporations/{corporation_id}/structures/skyhooks/{skyhook_id}': 3600,\n 'GET:corporations/{corporation_id}/structures/sovereignty-hubs': 3600,\n 'GET:corporations/{corporation_id}/structures/sovereignty-hubs/{sovereignty_hub_id}': 3600,\n 'GET:corporations/{corporation_id}/titles': 3600,\n 'GET:corporations/{corporation_id}/wallets': 300,\n 'GET:corporations/{corporation_id}/wallets/{division}/journal': 3600,\n 'GET:corporations/{corporation_id}/wallets/{division}/transactions': 3600,\n 'GET:cosmetics/skinr/{skinr_id}': 31536000,\n 'GET:fleets/{fleet_id}': 5,\n 'GET:fleets/{fleet_id}/members': 5,\n 'GET:fleets/{fleet_id}/wings': 5,\n 'GET:freelance-jobs/{job_id}': 60,\n 'GET:fw/systems': 1800,\n 'GET:incursions': 300,\n 'GET:industry/facilities': 3600,\n 'GET:industry/systems': 3600,\n 'GET:insurance/prices': 3600,\n 'GET:killmails/{killmail_id}/{killmail_hash}': 2592000,\n 'GET:markets/{region_id}/orders': 300,\n 'GET:markets/{region_id}/types': 600,\n 'GET:markets/prices': 3600,\n 'GET:markets/structures/{structure_id}': 300,\n 'GET:meta/changelog': 600,\n 'GET:meta/compatibility-dates': 600,\n 'GET:meta/name': 600,\n 'GET:military-campaigns': 60,\n 'GET:military-campaigns/{campaign_id}': 60,\n 'GET:military-campaigns/{campaign_id}/objectives': 60,\n 'GET:military-campaigns/{campaign_id}/objectives/{objective_id}': 60,\n 'GET:skyhooks/raidable': 300,\n 'GET:sovereignty/campaigns': 5,\n 'GET:sovereignty/systems': 300,\n 'GET:status': 30,\n 'GET:universe/schematics/{schematic_id}': 3600,\n 'GET:universe/structures': 3600,\n 'GET:universe/structures/{structure_id}': 3600,\n 'GET:universe/system_jumps': 3600,\n 'GET:universe/system_kills': 3600,\n 'GET:wars': 3600,\n 'GET:wars/{war_id}': 3600,\n 'GET:wars/{war_id}/killmails': 3600,\n};\n","export const PACKAGE_NAME = 'esi.ts';\nexport const PACKAGE_VERSION = '11.0.0'; // x-release-please-version\nexport const USER_AGENT: string = `${PACKAGE_NAME}/${PACKAGE_VERSION}`;\nexport const COMPATIBILITY_DATE = '2026-08-18';\n","import { ApiClient } from '../ApiClient';\nimport { EsiConfigurationError } from '../util/error';\nimport { logDebug } from '../logger/clientLog';\nimport { ICache } from '../cache/ICache';\nimport { buildConditionalCacheKey } from '../cache/cacheKey';\nimport { USER_AGENT, COMPATIBILITY_DATE } from '../constants';\n\n/**\n * Parse Cache-Control header to extract max-age TTL in milliseconds.\n */\nexport const parseCacheControlTtl = (\n headers: Record<string, string>,\n): number | undefined => {\n const cacheControl = headers['cache-control'] ?? headers['Cache-Control'];\n if (!cacheControl) return undefined;\n const match = /max-age=(\\d+)/.exec(cacheControl);\n return match ? parseInt(match[1]!, 10) * 1000 : undefined;\n};\n\n/** Printable ASCII and tab: a header value fetch accepts on every runtime. */\nconst HEADER_VALUE = /^[\\t\\x20-\\x7e]+$/;\n\n/**\n * Build the request headers for an ESI API call.\n */\nexport function buildRequestHeaders(\n client: ApiClient,\n url: string,\n method: string,\n requiresAuth: boolean,\n useETag: boolean,\n body: unknown,\n resolveCache: (client: ApiClient) => ICache | null,\n accept: string = 'application/json',\n): HeadersInit {\n // A configured userAgent names the application first; the library's own\n // identifier follows, as CCP asks. It was validated when configured.\n const userAgent = client.getUserAgent();\n const headers: HeadersInit = {\n Accept: accept,\n 'Accept-Encoding': 'gzip, deflate, br',\n 'User-Agent': userAgent ? `${userAgent} ${USER_AGENT}` : USER_AGENT,\n 'X-Compatibility-Date': client.getCompatibilityDate() ?? COMPATIBILITY_DATE,\n };\n\n // ESI reads the caller from X-User-Agent: the configured userAgent, else the\n // clientId (README.md). clientId is free text (config or ESI_CLIENT_ID), and\n // fetch rejects a header value holding a control character, so a clientId\n // that is not a legal header value is left off rather than failing every\n // request.\n const clientId = client.getClientId();\n if (userAgent) {\n headers['X-User-Agent'] = userAgent;\n } else if (HEADER_VALUE.test(clientId)) {\n headers['X-User-Agent'] = clientId;\n }\n\n const tenant = client.getTenant();\n if (tenant) {\n headers['X-Tenant'] = tenant;\n }\n\n const language = client.getLanguage();\n if (language) {\n headers['Accept-Language'] = language;\n }\n\n if (requiresAuth) {\n const authHeader = client.getAuthorizationHeader();\n if (!authHeader) {\n throw new EsiConfigurationError(\n 'NO_AUTH_TOKEN',\n [\n 'Authorization header is required for this endpoint but no access token is configured',\n 'Fix: set ESI_ACCESS_TOKEN in your environment (see .env.example), or call client.setAccessToken(token), or pass accessToken to the EsiClient constructor',\n ].join(' — '),\n );\n }\n headers['Authorization'] = authHeader;\n }\n\n const cache = resolveCache(client);\n if (useETag && method === 'GET' && cache) {\n // The claimed identity's ETag: for a token ESI has not accepted yet, the\n // 304 this earns is ESI's acceptance, and a 401 serves nothing.\n const key = buildConditionalCacheKey(url, client, requiresAuth);\n const cachedETag = cache.getETag(key);\n if (cachedETag) {\n headers['If-None-Match'] = cachedETag;\n logDebug(client, `Adding If-None-Match header: ${cachedETag}`, {\n etag: cachedETag,\n });\n }\n }\n\n if (body) {\n headers['Content-Type'] = 'application/json';\n }\n\n return headers;\n}\n","import { ApiClient } from '../ApiClient';\nimport { logDebug } from '../logger/clientLog';\nimport { ICache } from '../cache/ICache';\nimport { buildCacheKey } from '../cache/cacheKey';\nimport { ParsedHeaders } from '../util/headersUtil';\nimport { camelToSnake } from '../util/stringUtil';\nimport { esiCacheTtls } from '../endpoints/esi-cache-ttls.generated';\nimport { parseCacheControlTtl } from './headers';\n\nexport interface EsiHandlerResponse {\n headers: Record<string, string>;\n body: unknown;\n status?: number;\n fromCache?: boolean;\n stale?: boolean;\n cacheHitType?: 'spec-ttl' | 'etag-304' | 'stale-on-error';\n responseTimeMs?: number;\n cursors?: import('../pagination/CursorPaginationHandler').CursorTokens;\n}\n\n/**\n * How long a cached entry is kept after its freshness TTL elapses. Past the\n * freshness TTL the entry is no longer served without a request, but it still\n * supplies the ETag for an `If-None-Match` revalidation and the body served\n * stale when ESI answers with a 5xx. One hour spans ESI's daily downtime. The\n * cache's `maxEntries` still bounds memory.\n */\nconst STALE_RETENTION_MS = 60 * 60 * 1000;\n\n/**\n * The lifetime to store an entry for: its freshness TTL plus the stale\n * retention window. Undefined when the response gave no freshness TTL, so the\n * cache applies its own `defaultTtl`.\n */\nfunction retentionTtl(freshnessTtlMs: number | undefined): number | undefined {\n return freshnessTtlMs === undefined\n ? undefined\n : freshnessTtlMs + STALE_RETENTION_MS;\n}\n\n/**\n * Look up the spec-defined cache TTL for a given method + template path.\n * Returns TTL in milliseconds, or undefined if not found.\n */\nexport function lookupSpecTtl(\n method: string,\n templatePath: string,\n): number | undefined {\n const normalized = templatePath\n .replace(/\\/$/, '')\n .replace(/\\{(\\w+)\\}/g, (_, name: string) => `{${camelToSnake(name)}}`);\n const key = `${method}:${normalized}`;\n const seconds = esiCacheTtls[key];\n return typeof seconds === 'number' ? seconds * 1000 : undefined;\n}\n\n/**\n * Attempt a spec-aware cache hit (TTL-based, no network request).\n */\nexport function trySpecAwareCacheHit(\n client: ApiClient,\n url: string,\n method: string,\n templatePath: string | undefined,\n resolveCache: (client: ApiClient) => ICache | null,\n requiresAuth: boolean = false,\n): EsiHandlerResponse | null {\n if (method !== 'GET' || !templatePath) return null;\n const specTtlMs = lookupSpecTtl(method, templatePath);\n if (!specTtlMs) return null;\n const cache = resolveCache(client);\n if (!cache) return null;\n const key = buildCacheKey(url, client, requiresAuth);\n const entry = cache.get(key);\n if (!entry) return null;\n const age = Date.now() - entry.timestamp;\n if (age < specTtlMs) {\n logDebug(\n client,\n `Spec-aware cache hit for ${url} (age=${Math.round(age / 1000)}s, ttl=${Math.round(specTtlMs / 1000)}s)`,\n { method, templatePath },\n );\n return {\n headers: entry.headers,\n body: entry.data,\n status: 200,\n fromCache: true,\n cacheHitType: 'spec-ttl',\n };\n }\n return null;\n}\n\n/** Whether the cache holds an unexpired entry for the URL. */\nexport function hasCachedEntry(\n client: ApiClient,\n url: string,\n resolveCache: (client: ApiClient) => ICache | null,\n requiresAuth: boolean = false,\n authorization?: string,\n): boolean {\n const cache = resolveCache(client);\n return (\n !!cache &&\n cache.has(buildCacheKey(url, client, requiresAuth, authorization))\n );\n}\n\n/**\n * Attempt to return a stale cached response (used on server errors).\n */\nexport function tryStaleCacheResponse(\n client: ApiClient,\n url: string,\n parsed: ParsedHeaders,\n resolveCache: (client: ApiClient) => ICache | null,\n requiresAuth: boolean = false,\n authorization?: string,\n): EsiHandlerResponse | null {\n const cache = resolveCache(client);\n if (!cache) return null;\n const key = buildCacheKey(url, client, requiresAuth, authorization);\n const cachedEntry = cache.get(key);\n if (!cachedEntry) return null;\n return {\n headers: { ...cachedEntry.headers, ...parsed.raw },\n body: cachedEntry.data,\n status: 200,\n fromCache: true,\n stale: true,\n cacheHitType: 'stale-on-error',\n };\n}\n\n/**\n * Successful writes seen by a cache, so a read that was in flight when one of\n * them invalidated its path does not store the pre-write body afterwards.\n * `recent` keeps the last RECENT_WRITES paths; a read that outlived more\n * writes than that is treated as overtaken.\n */\ninterface WriteLog {\n generation: number;\n recent: { generation: number; path: string }[];\n}\n\nconst RECENT_WRITES = 64;\nconst writeLogs = new WeakMap<ICache, WriteLog>();\n\nfunction writeLogFor(cache: ICache): WriteLog {\n let log = writeLogs.get(cache);\n if (!log) {\n log = { generation: 0, recent: [] };\n writeLogs.set(cache, log);\n }\n return log;\n}\n\n/**\n * The client's write generation. Take it before sending a GET and pass it to\n * cacheResponse, which then declines to store the response if a write has\n * invalidated the same path since.\n */\nexport function currentWriteGeneration(\n client: ApiClient,\n resolveCache: (client: ApiClient) => ICache | null,\n): number {\n const cache = resolveCache(client);\n return cache ? writeLogFor(cache).generation : 0;\n}\n\nfunction invalidatedSince(\n cache: ICache,\n generation: number,\n key: string,\n): boolean {\n const log = writeLogFor(cache);\n if (log.generation === generation) return false;\n if (log.generation - generation > log.recent.length) return true;\n return log.recent.some(\n (write) => write.generation > generation && key.includes(write.path),\n );\n}\n\n/**\n * Cache a successful response, or invalidate cache for non-GET methods.\n * `sentAtWriteGeneration` (see currentWriteGeneration) skips storing a GET\n * response whose path a write invalidated while the request was in flight.\n * `authorization` is the header the request carried, which keys the entry\n * (see buildCacheKey).\n */\nexport function cacheResponse(\n client: ApiClient,\n url: string,\n method: string,\n endpoint: string,\n parsed: ParsedHeaders,\n data: unknown,\n useETag: boolean,\n resolveCache: (client: ApiClient) => ICache | null,\n templatePath?: string,\n requiresAuth: boolean = false,\n sentAtWriteGeneration?: number,\n authorization?: string,\n): void {\n const cache = resolveCache(client);\n if (useETag && method === 'GET' && cache && parsed.etag) {\n const key = buildCacheKey(url, client, requiresAuth, authorization);\n if (\n sentAtWriteGeneration !== undefined &&\n invalidatedSince(cache, sentAtWriteGeneration, key)\n ) {\n logDebug(\n client,\n `Not caching ${url}: a write invalidated it while the request was in flight`,\n { method },\n );\n return;\n }\n const headerTtl = parseCacheControlTtl(parsed.raw);\n const specTtlMs = templatePath\n ? lookupSpecTtl(method, templatePath)\n : undefined;\n const freshnessTtl = specTtlMs ?? headerTtl;\n const ttl = retentionTtl(freshnessTtl);\n cache.set(key, parsed.etag, data, parsed.raw, ttl);\n const ttlInfo =\n freshnessTtl !== undefined\n ? ` (ttl=${freshnessTtl}ms, kept for ${ttl}ms)`\n : '';\n logDebug(\n client,\n `Cached response for ${url} with ETag ${parsed.etag}${ttlInfo}`,\n {\n method,\n etag: parsed.etag,\n },\n );\n }\n\n invalidateAfterWrite(client, method, endpoint, resolveCache);\n}\n\n/**\n * Evict the cached copy of a response the caller rejected. `executeRequest`\n * caches a GET body before `createClient` validates it, so a body that fails\n * the endpoint's schema would otherwise be served again, from the spec TTL or\n * after a 304, until the entry expired.\n */\nexport function evictRejectedResponse(\n client: ApiClient,\n endpoint: string,\n requiresAuth: boolean,\n resolveCache: (client: ApiClient) => ICache | null,\n): void {\n const cache = resolveCache(client);\n if (!cache) return;\n const url = `${client.getLink()}/${endpoint}`;\n if (cache.delete(buildCacheKey(url, client, requiresAuth))) {\n logDebug(\n client,\n `Evicted cached response for ${url} after it failed validation`,\n );\n }\n}\n\n/**\n * Evict cached reads under the endpoint's path after a successful write, so a\n * read that follows a POST/PUT/DELETE fetches rather than serving the\n * pre-write copy. Called for every 2xx status, including the body-less\n * 201/204 replies that return before a response is cached.\n */\nexport function invalidateAfterWrite(\n client: ApiClient,\n method: string,\n endpoint: string,\n resolveCache: (client: ApiClient) => ICache | null,\n): void {\n if (method === 'GET') return;\n const path = endpoint.split('?')[0]!;\n\n // Before the cache, and whether or not there is one: a read that starts\n // after this write must not be handed the response to a read that started\n // before it. Deduplication would do exactly that, because its key is the\n // path and the path has not changed.\n client.getDeduplicator()?.detachByPath?.(path);\n\n const cache = resolveCache(client);\n if (cache) {\n cache.deleteByPath(path);\n const log = writeLogFor(cache);\n log.generation += 1;\n log.recent.push({ generation: log.generation, path });\n if (log.recent.length > RECENT_WRITES) log.recent.shift();\n }\n}\n","import { ApiClient } from '../ApiClient';\nimport { EsiError } from '../util/error';\nimport { logInfo, logWarn, logError } from '../logger/clientLog';\nimport { ICache } from '../cache/ICache';\nimport { buildCacheKey } from '../cache/cacheKey';\nimport { ParsedHeaders } from '../util/headersUtil';\nimport { toEsiError } from '../util/error';\nimport { tryStaleCacheResponse, EsiHandlerResponse } from './cachePolicy';\n\nexport const STATUS_MESSAGES: Record<number, string> = {\n 201: 'Created',\n 204: 'No Content',\n 304: 'Not Modified',\n 400: 'Bad Request',\n 401: 'Unauthorized',\n 403: 'Forbidden',\n 404: 'Resource not found',\n 420: 'Error Limited',\n 422: 'Unprocessable Entity',\n 429: 'Too many requests',\n 500: 'Internal server error',\n 502: 'Bad Gateway',\n 503: 'Service Unavailable',\n 504: 'Gateway Timeout',\n 520: 'Internal server error, did the request terminate too soon?',\n};\n\n/**\n * The text an error status is reported with: the client's own message, else\n * the reason phrase, else `HTTP <status>`. HTTP/2 carries no reason phrase, so\n * `statusText` is empty for any status missing from STATUS_MESSAGES.\n */\nexport function statusMessage(response: Response): string {\n return (\n STATUS_MESSAGES[response.status] ||\n response.statusText ||\n `HTTP ${response.status}`\n );\n}\n\n/**\n * Handle early-return HTTP statuses (201, 204, 304).\n */\nexport function handleEarlyStatus(\n client: ApiClient,\n status: number,\n url: string,\n parsed: ParsedHeaders,\n useETag: boolean,\n resolveCache: (client: ApiClient) => ICache | null,\n requiresAuth: boolean = false,\n authorization?: string,\n): EsiHandlerResponse | null {\n if (status === 201) {\n return { headers: parsed.raw, body: undefined, status: 201 };\n }\n\n if (status === 204) {\n logInfo(client, `No Content for endpoint: ${url}`, { status });\n return { headers: parsed.raw, body: undefined, status: 204 };\n }\n\n if (status === 304) {\n const cache = resolveCache(client);\n if (useETag && cache) {\n const key = buildCacheKey(url, client, requiresAuth, authorization);\n const cachedEntry = cache.get(key);\n if (cachedEntry) {\n logInfo(client, `Cache hit (304) for endpoint: ${url}`, {\n status,\n cacheHitType: 'etag-304',\n });\n const headers: Record<string, string> = {\n ...cachedEntry.headers,\n ...parsed.raw,\n };\n // The stored ETag names the stored body. A concurrent request may have\n // replaced the entry after this one was sent with an older\n // If-None-Match, so the 304's own ETag can name a body the cache no\n // longer holds and must not overwrite the stored one.\n if ('etag' in parsed.raw) headers['etag'] = cachedEntry.etag;\n // A 304 confirms the stored body is current: store it again so its\n // freshness TTL, and the retention window after it, start over.\n cache.set(\n key,\n cachedEntry.etag,\n cachedEntry.data,\n headers,\n cachedEntry.ttl,\n );\n return {\n headers,\n body: cachedEntry.data,\n status: 304,\n fromCache: true,\n cacheHitType: 'etag-304',\n };\n }\n }\n throw new EsiError(\n 304,\n 'Not Modified — no cached data available',\n url,\n parsed.requestId ?? undefined,\n );\n }\n\n return null;\n}\n\nconst MAX_REASON_LENGTH = 200;\n\n/**\n * The reason ESI gives for an error, from its `{ \"error\": \"...\" }` body.\n * Undefined for any other body. Consumes the response body.\n */\nexport async function readEsiErrorReason(\n response: Response,\n): Promise<string | undefined> {\n try {\n const parsed = JSON.parse(await response.text()) as unknown;\n const reason =\n typeof parsed === 'object' && parsed !== null\n ? (parsed as { error?: unknown }).error\n : undefined;\n if (typeof reason !== 'string') return undefined;\n const trimmed = reason.trim();\n return trimmed ? trimmed.slice(0, MAX_REASON_LENGTH) : undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Handle error HTTP responses (4xx, 5xx). May serve stale cache on 5xx.\n * `esiReason` (see readEsiErrorReason) is appended to the status message.\n */\nexport function handleErrorResponse(\n client: ApiClient,\n response: Response,\n url: string,\n parsed: ParsedHeaders,\n useETag: boolean,\n resolveCache: (client: ApiClient) => ICache | null,\n requiresAuth: boolean = false,\n esiReason?: string,\n authorization?: string,\n): EsiHandlerResponse | never {\n const text = statusMessage(response);\n const errorMessage = esiReason ? `${text}: ${esiReason}` : text;\n\n if (response.status >= 500 && useETag) {\n const staleResult = tryStaleCacheResponse(\n client,\n url,\n parsed,\n resolveCache,\n requiresAuth,\n authorization,\n );\n if (staleResult) {\n logWarn(client, `${errorMessage} for ${url} — serving stale cache`, {\n status: response.status,\n });\n return { ...staleResult, status: response.status };\n }\n }\n\n if (response.status === 420 || response.status === 429) {\n logWarn(client, `Rate limited (${response.status}) on ${url}`, {\n status: response.status,\n });\n }\n\n let message = errorMessage;\n if (response.status === 401) {\n message = `${errorMessage} — the access token was missing, expired, or lacks the required ESI scope. Fix: verify ESI_ACCESS_TOKEN, or configure onTokenRefresh for automatic refresh on 401`;\n } else if (response.status === 403) {\n message = `${errorMessage} — your access token does not have the OAuth scopes required for this endpoint. Check the scopes on your EVE SSO application`;\n }\n\n throw new EsiError(\n response.status,\n message,\n url,\n parsed.requestId ?? undefined,\n );\n}\n\n/**\n * Rethrow an EsiError unchanged; wrap anything else as an ESIJS_ERROR fault.\n */\nexport function wrapError(error: unknown, client?: ApiClient): never {\n if (error instanceof EsiError) {\n throw error;\n }\n const message = error instanceof Error ? error.message : String(error);\n logError(client, `Unexpected error: ${message}`);\n throw toEsiError(error);\n}\n","import { ApiClient } from '../ApiClient';\nimport { EsiConfigurationError } from '../util/error';\nimport { ICache } from '../cache/ICache';\nimport { IRateLimiter } from '../rateLimiter/IRateLimiter';\nimport { ICircuitBreaker } from '../circuitBreaker/ICircuitBreaker';\nimport { IRetryStrategy } from '../IRetryStrategy';\nimport { RetryStrategy } from '../RetryStrategy';\n\nexport function resolveCache(client: ApiClient): ICache | null {\n return client.getCache();\n}\n\nexport function resolveRateLimiter(client: ApiClient): IRateLimiter {\n const limiter = client.getRateLimiter();\n if (!limiter) {\n throw new EsiConfigurationError(\n 'CONFIGURATION_ERROR',\n 'No rate limiter configured on ApiClient. ' +\n 'Set one via apiClient.setRateLimiter(new RateLimiter()).',\n );\n }\n return limiter;\n}\n\nexport function resolveCircuitBreaker(\n client: ApiClient,\n): ICircuitBreaker | null {\n return client.getCircuitBreaker();\n}\n\nexport function resolveRetryStrategy(client: ApiClient): IRetryStrategy {\n return (\n client.getRetryStrategy() ??\n new RetryStrategy(client.getRetryConfig() ?? undefined)\n );\n}\n","/**\n * Pagination Handler for ESI API calls\n * Handles pagination with proper error handling and empty page detection\n */\n\nimport { ApiClient } from '../ApiClient';\nimport { logInfo, logWarn, logError } from '../logger/clientLog';\nimport { resolveRetryStrategy } from '../requestPipeline/dependencies';\n\nexport interface PaginationOptions {\n maxPages?: number;\n stopOnEmptyPage?: boolean;\n}\n\nexport type PageFetcher = (paginatedEndpoint: string) => Promise<unknown[]>;\n\nexport class PaginationHandler {\n private static readonly DEFAULT_OPTIONS: Required<PaginationOptions> = {\n maxPages: 1000,\n stopOnEmptyPage: true,\n };\n\n /**\n * Fetch remaining pages (2..totalPages) and combine with first page data.\n * The caller has already fetched page 1 and knows the total page count.\n */\n static async fetchRemainingPages(\n client: ApiClient,\n endpoint: string,\n method: string,\n requiresAuth: boolean,\n firstPageData: unknown[],\n totalPages: number,\n body: unknown,\n options: PaginationOptions,\n pageFetch: PageFetcher,\n templatePath?: string,\n ): Promise<unknown[]> {\n const opts = { ...this.DEFAULT_OPTIONS, ...options };\n const rateLimiter = client.getRateLimiter();\n const allData: unknown[] = [...firstPageData];\n\n const effectiveMaxPage = Math.min(totalPages, opts.maxPages);\n\n if (effectiveMaxPage <= 1) {\n return allData;\n }\n\n logInfo(client, `Fetching pages 2-${effectiveMaxPage} for ${endpoint}...`, {\n method,\n totalPages,\n });\n\n for (let page = 2; page <= effectiveMaxPage; page++) {\n try {\n if (rateLimiter) await rateLimiter.checkRateLimit(templatePath, method);\n\n const pageData = await this.fetchPageWithRetry(\n client,\n endpoint,\n method,\n requiresAuth,\n page,\n pageFetch,\n );\n\n if (opts.stopOnEmptyPage && (!pageData || pageData.length === 0)) {\n logWarn(client, `Page ${page} is empty. Stopping pagination.`, {\n page,\n });\n break;\n }\n\n allData.push(...pageData);\n logInfo(\n client,\n `Fetched page ${page}/${effectiveMaxPage} (${pageData.length} items)`,\n { page, items: pageData.length },\n );\n } catch (error) {\n logError(\n client,\n `Failed to fetch page ${page}: ${error instanceof Error ? error.message : String(error)}`,\n { page },\n );\n throw error instanceof Error\n ? error\n : new Error(`Failed to fetch page ${page}: ${String(error)}`);\n }\n }\n\n logInfo(\n client,\n `Pagination complete. Fetched ${allData.length} total items from up to ${effectiveMaxPage} pages.`,\n { totalItems: allData.length },\n );\n return allData;\n }\n\n /**\n * Fetch a single page with retry logic.\n *\n * Delegates to the configured `IRetryStrategy` (exponential backoff\n * with jitter) instead of implementing a custom retry loop.\n */\n private static async fetchPageWithRetry(\n client: ApiClient,\n endpoint: string,\n method: string,\n requiresAuth: boolean,\n page: number,\n pageFetch: PageFetcher,\n ): Promise<unknown[]> {\n const retryStrategy = resolveRetryStrategy(client);\n const paginatedEndpoint = this.buildPaginatedEndpoint(endpoint, page);\n\n return retryStrategy.execute(\n () => {\n logInfo(\n client,\n `Fetching page ${page} via pipeline: ${paginatedEndpoint}`,\n { page },\n );\n return pageFetch(paginatedEndpoint);\n },\n {\n client,\n endpoint: paginatedEndpoint,\n method,\n requiresAuth,\n refreshToken: client.hasTokenProvider()\n ? () => client.refreshToken().then(() => {})\n : undefined,\n },\n );\n }\n\n /**\n * Build a paginated endpoint path.\n * Preserves any existing query params on the endpoint and appends page=N.\n */\n private static buildPaginatedEndpoint(\n endpoint: string,\n page: number,\n ): string {\n const separator = endpoint.includes('?') ? '&' : '?';\n return `${endpoint}${separator}page=${page}`;\n }\n}\n","import { ApiClient } from '../ApiClient';\nimport { EsiError, EsiPaginationError } from '../util/error';\nimport { logInfo, logWarn } from '../logger/clientLog';\nimport { ParsedHeaders } from '../util/headersUtil';\nimport {\n PaginationHandler,\n PageFetcher,\n} from '../pagination/PaginationHandler';\nimport { CursorTokens } from '../pagination/CursorPaginationHandler';\nimport { ICache } from '../cache/ICache';\nimport { cacheResponse, EsiHandlerResponse } from './cachePolicy';\n\n/**\n * Handle cursor-based pagination. Returns null if the response\n * does not contain cursor pagination headers.\n */\nexport function handleCursorPagination(\n parsed: ParsedHeaders,\n data: unknown,\n): EsiHandlerResponse | null {\n if (!parsed.hasCursorPagination) return null;\n\n const cursors: CursorTokens = {\n before: parsed.cursorBefore,\n after: parsed.cursorAfter,\n };\n\n return { headers: parsed.raw, body: data, status: 200, cursors };\n}\n\n/**\n * Handle offset-based pagination. Fetches remaining pages if xPages > 1.\n */\nexport async function handleOffsetPagination(\n client: ApiClient,\n endpoint: string,\n method: string,\n requiresAuth: boolean,\n parsed: ParsedHeaders,\n data: unknown,\n body: unknown,\n url: string,\n useETag: boolean,\n pageFetch: PageFetcher,\n resolveCache: (client: ApiClient) => ICache | null,\n templatePath?: string,\n sentAtWriteGeneration?: number,\n authorization?: string,\n): Promise<EsiHandlerResponse> {\n const totalPages = parsed.xPages;\n\n if (totalPages <= 1) {\n return { headers: parsed.raw, body: data, status: 200 };\n }\n\n logInfo(\n client,\n `Found ${totalPages} pages, fetching additional pages with rate limiting...`,\n { endpoint, totalPages },\n );\n\n try {\n const firstPageData = Array.isArray(data) ? data : [data];\n const allData = await PaginationHandler.fetchRemainingPages(\n client,\n endpoint,\n method,\n requiresAuth,\n firstPageData,\n totalPages,\n body,\n {\n maxPages: Math.min(totalPages, 1000),\n stopOnEmptyPage: true,\n },\n pageFetch,\n templatePath,\n );\n\n cacheResponse(\n client,\n url,\n method,\n endpoint,\n parsed,\n allData,\n useETag,\n resolveCache,\n templatePath,\n requiresAuth,\n sentAtWriteGeneration,\n authorization,\n );\n return { headers: parsed.raw, body: allData, status: 200 };\n } catch (paginationError: unknown) {\n const msg =\n paginationError instanceof Error\n ? paginationError.message\n : String(paginationError);\n logWarn(client, `Pagination failed for ${url}: ${msg}`, {\n url,\n endpoint,\n error: msg,\n });\n if (paginationError instanceof EsiError) {\n throw paginationError;\n }\n throw new EsiPaginationError(url, paginationError);\n }\n}\n","import { ApiClient } from '../ApiClient';\nimport { RequestContext, ResponseContext } from '../middleware/Middleware';\nimport { EsiHandlerResponse } from './cachePolicy';\n\n/**\n * Apply request interceptors from the middleware pipeline.\n */\nexport async function applyRequestMiddleware(\n client: ApiClient,\n url: string,\n endpoint: string,\n method: string,\n headers: Record<string, string>,\n body: unknown,\n): Promise<{ url: string; headers: Record<string, string>; body: unknown }> {\n const middleware = client.getMiddleware();\n if (!middleware.hasInterceptors()) {\n return { url, headers, body };\n }\n const reqCtx: RequestContext = {\n url,\n endpoint,\n method,\n headers: { ...headers },\n body,\n };\n const modified = await middleware.applyRequestInterceptors(reqCtx);\n return {\n url: modified.url,\n headers: modified.headers,\n body: modified.body,\n };\n}\n\n/**\n * Apply response interceptors from the middleware pipeline.\n */\nexport async function applyResponseInterceptors(\n client: ApiClient,\n result: EsiHandlerResponse,\n url: string,\n endpoint: string,\n method: string,\n startTime: number,\n): Promise<EsiHandlerResponse> {\n const middleware = client.getMiddleware();\n if (!middleware.hasInterceptors()) return result;\n\n const responseCtx: ResponseContext = {\n url,\n endpoint,\n method,\n status: result.status ?? 200,\n headers: result.headers,\n body: result.body,\n durationMs: Date.now() - startTime,\n fromCache: result.fromCache ?? false,\n };\n\n const modified = await middleware.applyResponseInterceptors(responseCtx);\n return {\n ...result,\n headers: modified.headers,\n body: modified.body,\n };\n}\n","export interface EsiWarning {\n code: number;\n message: string;\n}\n\nexport interface ParsedHeaders {\n raw: Record<string, string>;\n xPages: number;\n etag: string | null;\n cacheControl: string | null;\n expires: string | null;\n lastModified: string | null;\n cursorBefore: string | null;\n cursorAfter: string | null;\n hasCursorPagination: boolean;\n warning: EsiWarning | null;\n requestId: string | null;\n date: string | null;\n contentLanguage: string | null;\n}\n\nexport function parseWarning(\n value: string | null | undefined,\n): EsiWarning | null {\n if (!value) return null;\n const match =\n /^(\\d{3}) - \"([^\"]+)\"$/.exec(value) ??\n /^(\\d{3}) - ([^ ].+[^ ])$/.exec(value);\n if (!match) return null;\n const [, code, message] = match;\n return { code: parseInt(code!, 10), message: message! };\n}\n\nexport function parseHeaders(fetchHeaders: Headers): ParsedHeaders {\n const raw: Record<string, string> = {};\n fetchHeaders.forEach((value, key) => {\n raw[key.toLowerCase()] = value;\n });\n\n const cursorBefore = raw['x-cursor-before'] ?? null;\n const cursorAfter = raw['x-cursor-after'] ?? null;\n\n return {\n raw,\n xPages: parseInt(raw['x-pages'] ?? '1', 10),\n etag: raw['etag'] ?? null,\n cacheControl: raw['cache-control'] ?? null,\n expires: raw['expires'] ?? null,\n lastModified: raw['last-modified'] ?? null,\n cursorBefore,\n cursorAfter,\n hasCursorPagination: 'x-cursor-before' in raw || 'x-cursor-after' in raw,\n warning: parseWarning(raw['warning']),\n requestId: raw['x-esi-request-id'] ?? null,\n date: raw['date'] ?? null,\n contentLanguage: raw['content-language'] ?? null,\n };\n}\n","import { ApiClient } from '../ApiClient';\nimport { markTokenAccepted } from '../cache/cacheKey';\nimport {\n EsiError,\n EsiNetworkError,\n EsiParseError,\n TimeoutError,\n} from '../util/error';\nimport { logInfo, logWarn, logError } from '../logger/clientLog';\nimport { parseHeaders, ParsedHeaders } from '../util/headersUtil';\nimport { ICache } from '../cache/ICache';\nimport { IRateLimiter } from '../rateLimiter/IRateLimiter';\nimport { ICircuitBreaker } from '../circuitBreaker/ICircuitBreaker';\nimport { buildRequestHeaders } from './headers';\nimport { applyRequestMiddleware } from './middlewareBridge';\nimport { readEsiErrorReason, statusMessage } from './statusHandling';\n\nexport interface RawFetchResult {\n response: Response;\n parsed: ParsedHeaders;\n url: string;\n /** The Authorization header the request carried, if any. */\n authorization?: string | undefined;\n}\n\nexport interface SingleFetchResult {\n data: unknown;\n parsed: ParsedHeaders;\n url: string;\n}\n\n/**\n * An abort from the request timer. Matched by name because DOMException is not\n * an `instanceof Error` in every runtime.\n */\nfunction isAbortError(err: unknown): boolean {\n return (\n typeof err === 'object' &&\n err !== null &&\n (err as { name?: unknown }).name === 'AbortError'\n );\n}\n\n/**\n * A request that never produced an HTTP response (DNS, connection reset, TLS).\n * Status 0 makes it an EsiError like every other request failure, and\n * retryable for GET; the original error is kept as `cause`.\n */\nfunction networkError(err: unknown, url: string): EsiNetworkError {\n const reason = err instanceof Error ? err.message : String(err);\n return new EsiNetworkError(reason, url, err);\n}\n\n/**\n * Read the whole body while the request timer is still running, then let\n * `text()` and `json()` answer from that copy. Later stages read the body\n * after this function returns, when a stalled or reset stream would no longer\n * be under the timeout or classed as a network failure.\n */\nasync function bufferBody(response: Response): Promise<void> {\n if (typeof response.text !== 'function') return;\n const text = await response.text();\n Object.defineProperties(response, {\n text: { value: () => Promise.resolve(text), configurable: true },\n json: {\n value: () => Promise.resolve(text).then((t) => JSON.parse(t) as unknown),\n configurable: true,\n },\n });\n}\n\n/**\n * Execute a single HTTP fetch with rate limiting, circuit breaker, and timeout.\n */\nexport async function executeSingleFetch(\n client: ApiClient,\n endpoint: string,\n method: string,\n body: unknown,\n requiresAuth: boolean,\n useETag: boolean,\n resolveCache: (client: ApiClient) => ICache | null,\n resolveRateLimiter: (client: ApiClient) => IRateLimiter,\n resolveCircuitBreaker: (client: ApiClient) => ICircuitBreaker | null,\n requestTimeout?: number,\n templatePath?: string,\n accept?: string,\n): Promise<RawFetchResult> {\n const rawUrl = `${client.getLink()}/${endpoint}`;\n const builtHeaders = buildRequestHeaders(\n client,\n rawUrl,\n method,\n requiresAuth,\n useETag,\n body,\n resolveCache,\n accept,\n ) as Record<string, string>;\n\n const req = await applyRequestMiddleware(\n client,\n rawUrl,\n endpoint,\n method,\n builtHeaders,\n body,\n );\n\n const options: RequestInit = {\n method,\n headers: req.headers,\n ...(req.body ? { body: JSON.stringify(req.body) } : {}),\n };\n\n const url = req.url;\n logInfo(client, `Hitting endpoint: ${url}`, { method, endpoint });\n\n const cb = resolveCircuitBreaker(client);\n const cbKey =\n cb && templatePath && cb.getKeyStrategy?.() === 'template'\n ? templatePath\n : endpoint;\n\n if (cb) cb.checkCircuit(cbKey);\n\n let cbRecorded = false;\n\n try {\n const rateLimiter = resolveRateLimiter(client);\n await rateLimiter.checkRateLimit(templatePath, method, req.headers);\n\n const timeoutMs = requestTimeout ?? client.getTimeout();\n const controller = new AbortController();\n const timer = globalThis.setTimeout(() => controller.abort(), timeoutMs);\n options.signal = controller.signal;\n\n let response: Response;\n try {\n response = await client.getFetch()(url, options);\n // The timeout covers the body as well as the headers, and a body that\n // fails to arrive is a network failure, not a malformed response.\n await bufferBody(response);\n } catch (err) {\n clearTimeout(timer);\n if (cb) {\n cb.recordFailure(cbKey, 0);\n cbRecorded = true;\n }\n // A transport that already speaks the SDK's error language (a test\n // double refusing a request it has no answer for) is passed through:\n // wrapping it as a network failure would rename it and retry it.\n if (err instanceof EsiError) throw err;\n if (isAbortError(err)) {\n throw new TimeoutError(timeoutMs, url);\n }\n throw networkError(err, url);\n }\n clearTimeout(timer);\n\n const parsed = parseHeaders(response.headers);\n\n // A 2xx or 304 to a bearer token is ESI accepting it: from here the\n // character it names keys what this client serves without a request.\n if (requiresAuth && (response.ok || response.status === 304)) {\n const sent = req.headers['Authorization'];\n if (sent) markTokenAccepted(client, sent);\n }\n\n rateLimiter.updateFromResponse(\n parsed.raw,\n response.status,\n templatePath,\n method,\n req.headers,\n );\n\n if (cb) {\n if (\n response.status >= 500 ||\n response.status === 420 ||\n response.status === 429\n ) {\n cb.recordFailure(cbKey, response.status);\n } else {\n cb.recordSuccess(cbKey);\n }\n cbRecorded = true;\n }\n\n if (parsed.warning) {\n logWarn(\n client,\n `ESI Warning ${parsed.warning.code}: ${parsed.warning.message}`,\n {\n url,\n warningCode: parsed.warning.code,\n },\n );\n }\n\n return {\n response,\n parsed,\n url,\n authorization: req.headers['Authorization'],\n };\n } finally {\n if (cb && !cbRecorded) {\n cb.recordFailure(cbKey, 0);\n }\n }\n}\n\n/**\n * Parse a JSON response body, throwing on parse failure.\n */\nexport async function parseJsonBody(\n client: ApiClient | null | undefined,\n response: Response,\n _url: string,\n): Promise<unknown> {\n try {\n return (await response.json()) as unknown;\n } catch (jsonError) {\n const msg =\n jsonError instanceof Error ? jsonError.message : String(jsonError);\n logError(client, `Failed to parse JSON response: ${msg}`, { url: _url });\n throw new EsiParseError(`Invalid JSON response: ${msg}`, _url, jsonError);\n }\n}\n\n/**\n * Fetch a single page: execute the fetch, check for errors, parse JSON.\n */\nexport async function fetchOnePage(\n client: ApiClient,\n endpoint: string,\n method: string,\n body: unknown,\n requiresAuth: boolean,\n useETag: boolean,\n resolveCache: (client: ApiClient) => ICache | null,\n resolveRateLimiter: (client: ApiClient) => IRateLimiter,\n resolveCircuitBreaker: (client: ApiClient) => ICircuitBreaker | null,\n requestTimeout?: number,\n templatePath?: string,\n): Promise<SingleFetchResult> {\n const { response, parsed, url } = await executeSingleFetch(\n client,\n endpoint,\n method,\n body,\n requiresAuth,\n useETag,\n resolveCache,\n resolveRateLimiter,\n resolveCircuitBreaker,\n requestTimeout,\n templatePath,\n );\n\n if (!response.ok) {\n const text = statusMessage(response);\n const reason = await readEsiErrorReason(response);\n throw new EsiError(\n response.status,\n reason ? `${text}: ${reason}` : text,\n url,\n parsed.requestId ?? undefined,\n );\n }\n\n const data = await parseJsonBody(client, response, url);\n return { data, parsed, url };\n}\n","import { ApiClient } from './ApiClient';\nimport { buildDedupeKey } from './cache/cacheKey';\nimport { logInfo } from './logger/clientLog';\n\nimport {\n trySpecAwareCacheHit,\n cacheResponse,\n currentWriteGeneration,\n hasCachedEntry,\n invalidateAfterWrite,\n handleEarlyStatus,\n handleErrorResponse,\n readEsiErrorReason,\n wrapError,\n handleCursorPagination,\n handleOffsetPagination,\n applyResponseInterceptors,\n executeSingleFetch,\n fetchOnePage,\n parseJsonBody,\n resolveCache,\n resolveRateLimiter,\n resolveCircuitBreaker,\n resolveRetryStrategy,\n} from './requestPipeline';\nexport type { EsiHandlerResponse } from './requestPipeline';\nimport type { EsiHandlerResponse } from './requestPipeline';\nimport type { TextResponse } from './endpoints/EndpointDefinition';\n\n// --- Main request orchestration ---\n\nconst executeRequest = async (\n client: ApiClient,\n endpoint: string,\n method: string,\n body?: unknown,\n requiresAuth: boolean = false,\n useETag: boolean = true,\n requestTimeout?: number,\n templatePath?: string,\n textResponse?: TextResponse,\n emptyWhenNoContent?: boolean,\n): Promise<EsiHandlerResponse> => {\n const startTime = Date.now();\n const finish = (r: EsiHandlerResponse) => {\n r.responseTimeMs = Date.now() - startTime;\n const rawUrl = `${client.getLink()}/${endpoint}`;\n return applyResponseInterceptors(\n client,\n r,\n rawUrl,\n endpoint,\n method,\n startTime,\n );\n };\n\n try {\n const writeGeneration = currentWriteGeneration(client, resolveCache);\n const revalidating =\n useETag &&\n method === 'GET' &&\n hasCachedEntry(\n client,\n `${client.getLink()}/${endpoint}`,\n resolveCache,\n requiresAuth,\n );\n const { response, parsed, url, authorization } = await executeSingleFetch(\n client,\n endpoint,\n method,\n body,\n requiresAuth,\n useETag,\n resolveCache,\n resolveRateLimiter,\n resolveCircuitBreaker,\n requestTimeout,\n templatePath,\n textResponse?.accept,\n );\n\n if (response.status === 201 || response.status === 204) {\n invalidateAfterWrite(client, method, endpoint, resolveCache);\n }\n\n if (response.status === 201) {\n let data: unknown;\n try {\n data = (await response.json()) as unknown;\n } catch {\n data = undefined;\n }\n return finish({ headers: parsed.raw, body: data, status: 201 });\n }\n\n // ESI answers an expired public contract with a 200 and Content-Length 0\n // where its spec says 204. Only endpoints that opt in read that as no\n // content; elsewhere an empty body is a JSON_PARSE_ERROR.\n if (\n emptyWhenNoContent &&\n response.status === 200 &&\n response.headers.get('content-length') === '0'\n ) {\n logInfo(client, `No Content for endpoint: ${url}`, { status: 200 });\n return finish({ headers: parsed.raw, body: undefined, status: 200 });\n }\n\n if (\n response.status === 304 &&\n revalidating &&\n !hasCachedEntry(client, url, resolveCache, requiresAuth, authorization)\n ) {\n // The entry this request revalidated left the cache while it was in\n // flight (a write to its path evicted it, or it expired), so the 304 has\n // no body to stand for. The repeat finds no entry, sends no\n // If-None-Match and gets the current representation.\n return await executeRequest(\n client,\n endpoint,\n method,\n body,\n requiresAuth,\n useETag,\n requestTimeout,\n templatePath,\n textResponse,\n emptyWhenNoContent,\n );\n }\n\n const earlyResult = handleEarlyStatus(\n client,\n response.status,\n url,\n parsed,\n useETag,\n resolveCache,\n requiresAuth,\n authorization,\n );\n if (earlyResult) return finish(earlyResult);\n\n if (!response.ok) {\n const staleOrThrow = handleErrorResponse(\n client,\n response,\n url,\n parsed,\n useETag,\n resolveCache,\n requiresAuth,\n await readEsiErrorReason(response),\n authorization,\n );\n return finish(staleOrThrow);\n }\n\n const data = textResponse\n ? await response.text()\n : await parseJsonBody(client, response, url);\n // A multi-page response is cached by handleOffsetPagination once every\n // page is in. Caching page 1 here would let a retried call revalidate\n // against page 1 alone and resolve with it after a 304.\n if (parsed.xPages <= 1 || parsed.hasCursorPagination) {\n cacheResponse(\n client,\n url,\n method,\n endpoint,\n parsed,\n data,\n useETag,\n resolveCache,\n templatePath,\n requiresAuth,\n writeGeneration,\n authorization,\n );\n }\n\n const cursorResult = handleCursorPagination(parsed, data);\n if (cursorResult) return finish(cursorResult);\n\n const pageFetch = async (paginatedEndpoint: string): Promise<unknown[]> => {\n const result = await fetchOnePage(\n client,\n paginatedEndpoint,\n method,\n body,\n requiresAuth,\n false,\n resolveCache,\n resolveRateLimiter,\n resolveCircuitBreaker,\n requestTimeout,\n templatePath,\n );\n return Array.isArray(result.data)\n ? (result.data as unknown[])\n : [result.data];\n };\n\n const paginatedResult = await handleOffsetPagination(\n client,\n endpoint,\n method,\n requiresAuth,\n parsed,\n data,\n body,\n url,\n useETag,\n pageFetch,\n resolveCache,\n templatePath,\n writeGeneration,\n authorization,\n );\n return finish(paginatedResult);\n } catch (error: unknown) {\n wrapError(error, client);\n }\n};\n\nexport const handleSinglePageRequest = async (\n client: ApiClient,\n endpoint: string,\n method: string,\n body?: unknown,\n requiresAuth: boolean = false,\n templatePath?: string,\n requestTimeout?: number,\n): Promise<EsiHandlerResponse> => {\n // No If-None-Match: this path keeps no cache of its own, so a 304 would\n // leave it with no body to return (#292).\n const doExecute = () =>\n fetchOnePage(\n client,\n endpoint,\n method,\n body,\n requiresAuth,\n false,\n resolveCache,\n resolveRateLimiter,\n resolveCircuitBreaker,\n requestTimeout,\n templatePath,\n ).then(({ data, parsed }) => ({\n headers: parsed.raw,\n body: data,\n }));\n\n const retryStrategy = resolveRetryStrategy(client);\n\n return retryStrategy.execute<EsiHandlerResponse>(doExecute, {\n client,\n endpoint,\n method,\n requiresAuth,\n refreshToken: client.hasTokenProvider()\n ? () => client.refreshToken().then(() => {})\n : undefined,\n });\n};\n\nexport const handleRequest = async (\n client: ApiClient,\n endpoint: string,\n method: string,\n body?: unknown,\n requiresAuth: boolean = false,\n useETag: boolean = true,\n templatePath?: string,\n requestTimeout?: number,\n textResponse?: TextResponse,\n emptyWhenNoContent: boolean = false,\n): Promise<EsiHandlerResponse> => {\n const rawUrl = `${client.getLink()}/${endpoint}`;\n const startTime = Date.now();\n const specCacheHit = (): Promise<EsiHandlerResponse> | null => {\n const hit = trySpecAwareCacheHit(\n client,\n rawUrl,\n method,\n templatePath,\n resolveCache,\n requiresAuth,\n );\n return hit\n ? applyResponseInterceptors(\n client,\n hit,\n rawUrl,\n endpoint,\n method,\n startTime,\n )\n : null;\n };\n const specHit = specCacheHit();\n if (specHit) return specHit;\n\n const doExecute = () =>\n executeRequest(\n client,\n endpoint,\n method,\n body,\n requiresAuth,\n useETag,\n requestTimeout,\n templatePath,\n textResponse,\n emptyWhenNoContent,\n );\n\n const dedup = client.getDeduplicator();\n const canDedup = dedup && method === 'GET' && !body;\n\n let attempted = false;\n const operation = () => {\n // A retry waited out a backoff, during which a concurrent call may have\n // cached a fresh copy. Serve it rather than spend another request.\n const retryHit = attempted ? specCacheHit() : null;\n attempted = true;\n if (retryHit) return retryHit;\n // Keyed by identity as well as endpoint: one client can hold more than one\n // token over its life, and two concurrent authenticated GETs under\n // different tokens must not be answered from one response.\n return canDedup\n ? dedup.dedupe<EsiHandlerResponse>(\n buildDedupeKey(endpoint, client, requiresAuth),\n doExecute,\n )\n : doExecute();\n };\n\n const retryStrategy = resolveRetryStrategy(client);\n\n return retryStrategy.execute<EsiHandlerResponse>(operation, {\n client,\n endpoint,\n method,\n requiresAuth,\n refreshToken: client.hasTokenProvider()\n ? () => client.refreshToken().then(() => {})\n : undefined,\n });\n};\n","import { ApiClient } from '../ApiClient';\nimport { handleSinglePageRequest } from '../ApiRequestHandler';\nimport { logInfo } from '../logger/clientLog';\nimport { EsiValidationError } from '../util/error';\nimport type { EndpointDefinition } from '../endpoints/EndpointDefinition';\n\nexport interface PageResult<T = unknown> {\n data: T[];\n page: number;\n totalPages: number;\n}\n\ntype ResponseSchema = NonNullable<EndpointDefinition['responseSchema']>;\n\nfunction extractArray<T>(body: unknown): T[] {\n if (Array.isArray(body)) return body as T[];\n if (body !== undefined && body !== null) return [body as T];\n return [];\n}\n\nfunction validatePageBody(\n body: unknown,\n schema: ResponseSchema | undefined,\n url: string,\n validate: boolean,\n): unknown {\n if (!schema || !validate) return body;\n const result = schema.safeParse(body);\n if (!result.success) {\n throw new EsiValidationError(url, result.error);\n }\n return result.data;\n}\n\nexport async function fetchAllPages<T = unknown>(\n client: ApiClient,\n endpoint: string,\n method: string,\n requiresAuth: boolean = false,\n body?: unknown,\n templatePath?: string,\n responseSchema?: ResponseSchema,\n concurrency: number = 8,\n): Promise<T[]> {\n const validate = client.getValidateResponse();\n const firstResponse = await handleSinglePageRequest(\n client,\n endpoint,\n method,\n body,\n requiresAuth,\n templatePath,\n );\n\n const firstBody = validatePageBody(\n firstResponse.body,\n responseSchema,\n `${client.getLink()}/${endpoint}`,\n validate,\n );\n\n const totalPages = parseInt(firstResponse.headers['x-pages'] || '1', 10);\n const result: T[] = extractArray<T>(firstBody);\n\n // Empty for one page (or an unreadable X-Pages, which parses to NaN), so\n // the loop below does nothing and the first page is the whole result.\n const remaining = Array.from({ length: totalPages - 1 }, (_, i) => i + 2);\n\n // Stryker disable next-line EqualityOperator: `<=` adds one pass over an empty slice, which fetches nothing and pushes nothing.\n for (let i = 0; i < remaining.length; i += concurrency) {\n const batch = remaining.slice(i, i + concurrency);\n const pages = await Promise.all(\n batch.map(async (page) => {\n const sep = endpoint.includes('?') ? '&' : '?';\n const pagedEndpoint = `${endpoint}${sep}page=${page}`;\n\n logInfo(\n client,\n `Fetching page ${page}/${totalPages}: ${pagedEndpoint}`,\n { page, totalPages },\n );\n\n const response = await handleSinglePageRequest(\n client,\n pagedEndpoint,\n method,\n body,\n requiresAuth,\n templatePath,\n );\n\n return validatePageBody(\n response.body,\n responseSchema,\n `${client.getLink()}/${pagedEndpoint}`,\n validate,\n );\n }),\n );\n\n for (const pageBody of pages) {\n result.push(...extractArray<T>(pageBody));\n }\n }\n\n return result;\n}\n\nexport async function* fetchPages<T = unknown>(\n client: ApiClient,\n endpoint: string,\n method: string,\n requiresAuth: boolean = false,\n body?: unknown,\n templatePath?: string,\n responseSchema?: ResponseSchema,\n): AsyncGenerator<PageResult<T>, void, undefined> {\n const validate = client.getValidateResponse();\n const firstResponse = await handleSinglePageRequest(\n client,\n endpoint,\n method,\n body,\n requiresAuth,\n templatePath,\n );\n\n const firstBody = validatePageBody(\n firstResponse.body,\n responseSchema,\n `${client.getLink()}/${endpoint}`,\n validate,\n );\n\n const totalPages = parseInt(firstResponse.headers['x-pages'] || '1', 10);\n\n yield { data: extractArray<T>(firstBody), page: 1, totalPages };\n\n for (let page = 2; page <= totalPages; page++) {\n const sep = endpoint.includes('?') ? '&' : '?';\n const pagedEndpoint = `${endpoint}${sep}page=${page}`;\n\n logInfo(client, `Fetching page ${page}/${totalPages}: ${pagedEndpoint}`, {\n page,\n totalPages,\n });\n\n const response = await handleSinglePageRequest(\n client,\n pagedEndpoint,\n method,\n body,\n requiresAuth,\n templatePath,\n );\n\n const pageBody = validatePageBody(\n response.body,\n responseSchema,\n `${client.getLink()}/${pagedEndpoint}`,\n validate,\n );\n const pageData = extractArray<T>(pageBody);\n if (pageData.length === 0) break;\n\n yield { data: pageData, page, totalPages };\n }\n}\n","import { ApiClient } from '../ApiClient';\nimport { logInfo, logDebug } from '../logger/clientLog';\nimport { ICache, CacheEntry } from './ICache';\n\nexport type { CacheEntry } from './ICache';\n\nexport interface ETagCacheConfig {\n maxEntries?: number | undefined;\n defaultTtl?: number | undefined;\n cleanupInterval?: number | undefined;\n}\n\nexport class ETagCacheManager implements ICache {\n private cache: Map<string, CacheEntry> = new Map();\n private config: { [K in keyof ETagCacheConfig]-?: number };\n private cleanupTimer?: NodeJS.Timeout | undefined;\n private hits: number = 0;\n private misses: number = 0;\n private client: ApiClient | null = null;\n\n setClient(client: ApiClient | null): void {\n this.client = client;\n }\n\n private log(\n level: 'info' | 'debug',\n message: string,\n context?: Record<string, unknown>,\n ): void {\n const ctx =\n context && Object.keys(context).length > 0 ? context : undefined;\n const emit = level === 'info' ? logInfo : logDebug;\n emit(this.client, message, ctx);\n }\n\n /**\n * @param client - the client whose logger this cache logs to, from the\n * first line (the startup log) on. `setClient` can attach or change it\n * later; without one the cache logs to the global logger.\n */\n constructor(config: ETagCacheConfig = {}, client?: ApiClient | null) {\n this.client = client ?? null;\n this.config = {\n maxEntries: config.maxEntries ?? 1000,\n defaultTtl: config.defaultTtl ?? 5 * 60 * 1000, // 5 minutes default\n cleanupInterval: config.cleanupInterval ?? 60 * 1000, // 1 minute cleanup\n };\n\n this.startCleanupTimer();\n this.log(\n 'info',\n `ETag cache manager initialized with ${this.config.maxEntries} max entries`,\n { maxEntries: this.config.maxEntries },\n );\n }\n\n /**\n * Get cached data if ETag matches\n */\n get(url: string): CacheEntry | null {\n const entry = this.cache.get(url);\n\n if (!entry) {\n this.misses++;\n return null;\n }\n\n if (this.isExpired(entry)) {\n this.cache.delete(url);\n this.misses++;\n this.log('debug', `Cache entry expired for ${url}`, { url });\n return null;\n }\n\n this.hits++;\n this.log('debug', `Cache hit for ${url} with ETag ${entry.etag}`, { url });\n return entry;\n }\n\n /**\n * Get ETag for a URL (for If-None-Match header)\n */\n getETag(url: string): string | null {\n const entry = this.get(url);\n return entry ? entry.etag : null;\n }\n\n /**\n * Set cache entry\n */\n set(\n url: string,\n etag: string,\n data: unknown,\n headers: Record<string, string>,\n customTtl?: number,\n ): void {\n // Remove the oldest entry if the cache is full. Replacing an entry that is\n // already stored does not grow the cache, so it evicts nothing.\n if (!this.cache.has(url) && this.cache.size >= this.config.maxEntries) {\n this.evictOldest();\n }\n\n const entry: CacheEntry = {\n etag,\n data,\n headers,\n timestamp: Date.now(),\n ttl: customTtl ?? this.config.defaultTtl,\n };\n\n this.cache.set(url, entry);\n this.log('debug', `Cached response for ${url} with ETag ${etag}`, { url });\n }\n\n /**\n * Check if cached data exists and is valid\n */\n has(url: string): boolean {\n const entry = this.cache.get(url);\n return entry !== undefined && !this.isExpired(entry);\n }\n\n /**\n * Clear specific cache entry\n */\n delete(url: string): boolean {\n return this.cache.delete(url);\n }\n\n /**\n * Delete all cache entries whose URL contains the given path segment\n */\n deleteByPath(pathSegment: string): number {\n let count = 0;\n for (const key of this.cache.keys()) {\n if (key.includes(pathSegment)) {\n this.cache.delete(key);\n count++;\n }\n }\n if (count > 0) {\n this.log(\n 'debug',\n `Invalidated ${count} cache entries matching ${pathSegment}`,\n {\n count,\n },\n );\n }\n return count;\n }\n\n /**\n * Clear all cache entries\n */\n clear(): void {\n this.cache.clear();\n this.hits = 0;\n this.misses = 0;\n this.log('info', 'ETag cache cleared');\n }\n\n /**\n * Get cache statistics\n */\n getStats(): {\n totalEntries: number;\n maxEntries: number;\n hits: number;\n misses: number;\n hitRate: number;\n oldestEntry: number | null;\n newestEntry: number | null;\n } {\n let oldest = Infinity;\n let newest = -Infinity;\n for (const entry of this.cache.values()) {\n if (entry.timestamp < oldest) oldest = entry.timestamp;\n if (entry.timestamp > newest) newest = entry.timestamp;\n }\n const total = this.hits + this.misses;\n\n return {\n totalEntries: this.cache.size,\n maxEntries: this.config.maxEntries,\n hits: this.hits,\n misses: this.misses,\n hitRate: total > 0 ? this.hits / total : 0,\n oldestEntry: this.cache.size > 0 ? oldest : null,\n newestEntry: this.cache.size > 0 ? newest : null,\n };\n }\n\n /**\n * Update cache configuration\n */\n updateConfig(newConfig: Partial<ETagCacheConfig>): void {\n this.config = {\n maxEntries: newConfig.maxEntries ?? this.config.maxEntries,\n defaultTtl: newConfig.defaultTtl ?? this.config.defaultTtl,\n cleanupInterval: newConfig.cleanupInterval ?? this.config.cleanupInterval,\n };\n this.log('info', 'ETag cache configuration updated', {\n maxEntries: this.config.maxEntries,\n });\n }\n\n /**\n * Cleanup expired entries\n */\n cleanup(): number {\n const beforeSize = this.cache.size;\n\n for (const [url, entry] of this.cache.entries()) {\n if (this.isExpired(entry)) {\n this.cache.delete(url);\n }\n }\n\n const cleanedCount = beforeSize - this.cache.size;\n if (cleanedCount > 0) {\n this.log('debug', `Cleaned up ${cleanedCount} expired cache entries`, {\n cleanedCount,\n });\n }\n\n return cleanedCount;\n }\n\n /**\n * Shutdown cache manager\n */\n shutdown(): void {\n if (this.cleanupTimer) {\n clearInterval(this.cleanupTimer);\n }\n this.log('info', 'ETag cache manager shut down');\n }\n\n private isExpired(entry: CacheEntry): boolean {\n if (!entry.ttl) return false;\n return Date.now() - entry.timestamp > entry.ttl;\n }\n\n private evictOldest(): void {\n let oldestKey: string | null = null;\n let oldestTime = Infinity;\n\n for (const [key, entry] of this.cache.entries()) {\n if (entry.timestamp < oldestTime) {\n oldestTime = entry.timestamp;\n oldestKey = key;\n }\n }\n\n if (oldestKey) {\n this.cache.delete(oldestKey);\n this.log('debug', `Evicted oldest cache entry: ${oldestKey}`, {\n url: oldestKey,\n });\n }\n }\n\n private startCleanupTimer(): void {\n this.cleanupTimer = setInterval(() => {\n this.cleanup();\n }, this.config.cleanupInterval);\n // Don't prevent process exit (important for test runners and CLI tools)\n this.cleanupTimer.unref();\n }\n}\n","// Auto-generated from ESI OpenAPI spec — do not edit manually\n// Compatibility date: 2026-08-18\n// Endpoints with rate limit groups: 171\n\nexport interface RateLimitGroupSpec {\n group: string;\n maxTokens: number;\n windowSizeMs: number;\n}\n\nexport const esiRateLimitGroups: Record<string, RateLimitGroupSpec> = {\n 'GET:alliances/{alliance_id}/contacts': { group: 'alliance-social', maxTokens: 300, windowSizeMs: 900000 },\n 'GET:alliances/{alliance_id}/contacts/labels': { group: 'alliance-social', maxTokens: 300, windowSizeMs: 900000 },\n 'GET:characters/{character_id}/access-lists': { group: 'char-access', maxTokens: 600, windowSizeMs: 900000 },\n 'GET:characters/{character_id}/access-lists/{access_list_id}': { group: 'char-access', maxTokens: 600, windowSizeMs: 900000 },\n 'GET:characters/{character_id}/agents_research': { group: 'char-industry', maxTokens: 600, windowSizeMs: 900000 },\n 'GET:characters/{character_id}/assets': { group: 'char-asset', maxTokens: 1800, windowSizeMs: 900000 },\n 'POST:characters/{character_id}/assets/locations': { group: 'char-asset', maxTokens: 1800, windowSizeMs: 900000 },\n 'POST:characters/{character_id}/assets/names': { group: 'char-asset', maxTokens: 1800, windowSizeMs: 900000 },\n 'GET:characters/{character_id}/attributes': { group: 'char-detail', maxTokens: 600, windowSizeMs: 900000 },\n 'GET:characters/{character_id}/blueprints': { group: 'char-industry', maxTokens: 600, windowSizeMs: 900000 },\n 'GET:characters/{character_id}/calendar': { group: 'char-social', maxTokens: 600, windowSizeMs: 900000 },\n 'GET:characters/{character_id}/calendar/{event_id}': { group: 'char-social', maxTokens: 600, windowSizeMs: 900000 },\n 'PUT:characters/{character_id}/calendar/{event_id}': { group: 'char-social', maxTokens: 600, windowSizeMs: 900000 },\n 'GET:characters/{character_id}/calendar/{event_id}/attendees': { group: 'char-social', maxTokens: 600, windowSizeMs: 900000 },\n 'GET:characters/{character_id}/clones': { group: 'char-location', maxTokens: 1200, windowSizeMs: 900000 },\n 'GET:characters/{character_id}/contacts': { group: 'char-social', maxTokens: 600, windowSizeMs: 900000 },\n 'POST:characters/{character_id}/contacts': { group: 'char-social', maxTokens: 600, windowSizeMs: 900000 },\n 'PUT:characters/{character_id}/contacts': { group: 'char-social', maxTokens: 600, windowSizeMs: 900000 },\n 'DELETE:characters/{character_id}/contacts': { group: 'char-social', maxTokens: 600, windowSizeMs: 900000 },\n 'GET:characters/{character_id}/contacts/labels': { group: 'char-social', maxTokens: 600, windowSizeMs: 900000 },\n 'GET:characters/{character_id}/contracts': { group: 'char-contract', maxTokens: 600, windowSizeMs: 900000 },\n 'GET:characters/{character_id}/contracts/{contract_id}/bids': { group: 'char-contract', maxTokens: 600, windowSizeMs: 900000 },\n 'GET:characters/{character_id}/contracts/{contract_id}/items': { group: 'char-contract', maxTokens: 600, windowSizeMs: 900000 },\n 'GET:characters/{character_id}/cosmetics/skinr': { group: 'char-skinr', maxTokens: 30, windowSizeMs: 900000 },\n 'GET:characters/{character_id}/cosmetics/skinr/components': { group: 'char-skinr', maxTokens: 30, windowSizeMs: 900000 },\n 'POST:characters/{character_id}/cspa': { group: 'char-detail', maxTokens: 600, windowSizeMs: 900000 },\n 'GET:characters/{character_id}/fatigue': { group: 'char-location', maxTokens: 1200, windowSizeMs: 900000 },\n 'GET:characters/{character_id}/fittings': { group: 'fitting', maxTokens: 150, windowSizeMs: 900000 },\n 'POST:characters/{character_id}/fittings': { group: 'fitting', maxTokens: 150, windowSizeMs: 900000 },\n 'DELETE:characters/{character_id}/fittings/{fitting_id}': { group: 'fitting', maxTokens: 150, windowSizeMs: 900000 },\n 'GET:characters/{character_id}/fleet': { group: 'fleet', maxTokens: 1800, windowSizeMs: 900000 },\n 'GET:characters/{character_id}/freelance-jobs': { group: 'char-freelance-job', maxTokens: 300, windowSizeMs: 900000 },\n 'GET:characters/{character_id}/freelance-jobs/{job_id}/participation': { group: 'char-freelance-job', maxTokens: 300, windowSizeMs: 900000 },\n 'GET:characters/{character_id}/fw/stats': { group: 'factional-warfare', maxTokens: 150, windowSizeMs: 900000 },\n 'GET:characters/{character_id}/implants': { group: 'char-detail', maxTokens: 600, windowSizeMs: 900000 },\n 'GET:characters/{character_id}/industry/jobs': { group: 'char-industry', maxTokens: 600, windowSizeMs: 900000 },\n 'GET:characters/{character_id}/killmails/recent': { group: 'char-killmail', maxTokens: 30, windowSizeMs: 900000 },\n 'GET:characters/{character_id}/location': { group: 'char-location', maxTokens: 1200, windowSizeMs: 900000 },\n 'GET:characters/{character_id}/loyalty/points': { group: 'char-wallet', maxTokens: 150, windowSizeMs: 900000 },\n 'GET:characters/{character_id}/mail': { group: 'char-social', maxTokens: 600, windowSizeMs: 900000 },\n 'POST:characters/{character_id}/mail': { group: 'char-social', maxTokens: 600, windowSizeMs: 900000 },\n 'GET:characters/{character_id}/mail/{mail_id}': { group: 'char-social', maxTokens: 600, windowSizeMs: 900000 },\n 'PUT:characters/{character_id}/mail/{mail_id}': { group: 'char-social', maxTokens: 600, windowSizeMs: 900000 },\n 'DELETE:characters/{character_id}/mail/{mail_id}': { group: 'char-social', maxTokens: 600, windowSizeMs: 900000 },\n 'GET:characters/{character_id}/mail/labels': { group: 'char-social', maxTokens: 600, windowSizeMs: 900000 },\n 'POST:characters/{character_id}/mail/labels': { group: 'char-social', maxTokens: 600, windowSizeMs: 900000 },\n 'DELETE:characters/{character_id}/mail/labels/{label_id}': { group: 'char-social', maxTokens: 600, windowSizeMs: 900000 },\n 'GET:characters/{character_id}/mail/lists': { group: 'char-social', maxTokens: 600, windowSizeMs: 900000 },\n 'GET:characters/{character_id}/medals': { group: 'char-detail', maxTokens: 600, windowSizeMs: 900000 },\n 'GET:characters/{character_id}/mercenary-tactical-operations': { group: 'char-activity', maxTokens: 150, windowSizeMs: 900000 },\n 'GET:characters/{character_id}/mercenary-tactical-operations/{operation_id}': { group: 'char-activity', maxTokens: 150, windowSizeMs: 900000 },\n 'GET:characters/{character_id}/military-campaigns/objectives': { group: 'char-military-campaign', maxTokens: 150, windowSizeMs: 900000 },\n 'GET:characters/{character_id}/military-campaigns/objectives/{objective_id}': { group: 'char-military-campaign', maxTokens: 150, windowSizeMs: 900000 },\n 'GET:characters/{character_id}/mining': { group: 'char-industry', maxTokens: 600, windowSizeMs: 900000 },\n 'GET:characters/{character_id}/notifications': { group: 'char-notification', maxTokens: 15, windowSizeMs: 900000 },\n 'GET:characters/{character_id}/notifications/contacts': { group: 'char-social', maxTokens: 600, windowSizeMs: 900000 },\n 'GET:characters/{character_id}/online': { group: 'char-location', maxTokens: 1200, windowSizeMs: 900000 },\n 'GET:characters/{character_id}/paragon-hub/skinr': { group: 'char-paragon-hub', maxTokens: 150, windowSizeMs: 900000 },\n 'GET:characters/{character_id}/planets': { group: 'char-industry', maxTokens: 600, windowSizeMs: 900000 },\n 'GET:characters/{character_id}/planets/{planet_id}': { group: 'char-industry', maxTokens: 600, windowSizeMs: 900000 },\n 'GET:characters/{character_id}/portrait': { group: 'char-detail', maxTokens: 600, windowSizeMs: 900000 },\n 'GET:characters/{character_id}/roles': { group: 'char-detail', maxTokens: 600, windowSizeMs: 900000 },\n 'GET:characters/{character_id}/ship': { group: 'char-location', maxTokens: 1200, windowSizeMs: 900000 },\n 'GET:characters/{character_id}/skillqueue': { group: 'char-detail', maxTokens: 600, windowSizeMs: 900000 },\n 'GET:characters/{character_id}/skills': { group: 'char-detail', maxTokens: 600, windowSizeMs: 900000 },\n 'GET:characters/{character_id}/standings': { group: 'char-social', maxTokens: 600, windowSizeMs: 900000 },\n 'GET:characters/{character_id}/structures/mercenary-dens': { group: 'char-structure', maxTokens: 30, windowSizeMs: 900000 },\n 'GET:characters/{character_id}/structures/mercenary-dens/{mercenary_den_id}': { group: 'char-structure', maxTokens: 30, windowSizeMs: 900000 },\n 'GET:characters/{character_id}/titles': { group: 'char-detail', maxTokens: 600, windowSizeMs: 900000 },\n 'GET:characters/{character_id}/wallet': { group: 'char-wallet', maxTokens: 150, windowSizeMs: 900000 },\n 'GET:characters/{character_id}/wallet/journal': { group: 'char-wallet', maxTokens: 150, windowSizeMs: 900000 },\n 'GET:characters/{character_id}/wallet/transactions': { group: 'char-wallet', maxTokens: 150, windowSizeMs: 900000 },\n 'GET:corporation/{corporation_id}/mining/extractions': { group: 'corp-industry', maxTokens: 600, windowSizeMs: 900000 },\n 'GET:corporation/{corporation_id}/mining/observers': { group: 'corp-industry', maxTokens: 600, windowSizeMs: 900000 },\n 'GET:corporation/{corporation_id}/mining/observers/{observer_id}': { group: 'corp-industry', maxTokens: 600, windowSizeMs: 900000 },\n 'GET:corporations/{corporation_id}/assets': { group: 'corp-asset', maxTokens: 1800, windowSizeMs: 900000 },\n 'POST:corporations/{corporation_id}/assets/locations': { group: 'corp-asset', maxTokens: 1800, windowSizeMs: 900000 },\n 'POST:corporations/{corporation_id}/assets/names': { group: 'corp-asset', maxTokens: 1800, windowSizeMs: 900000 },\n 'GET:corporations/{corporation_id}/blueprints': { group: 'corp-industry', maxTokens: 600, windowSizeMs: 900000 },\n 'GET:corporations/{corporation_id}/contacts': { group: 'corp-social', maxTokens: 300, windowSizeMs: 900000 },\n 'GET:corporations/{corporation_id}/contacts/labels': { group: 'corp-social', maxTokens: 300, windowSizeMs: 900000 },\n 'GET:corporations/{corporation_id}/containers/logs': { group: 'corp-structure', maxTokens: 300, windowSizeMs: 900000 },\n 'GET:corporations/{corporation_id}/contracts': { group: 'corp-contract', maxTokens: 600, windowSizeMs: 900000 },\n 'GET:corporations/{corporation_id}/contracts/{contract_id}/bids': { group: 'corp-contract', maxTokens: 600, windowSizeMs: 900000 },\n 'GET:corporations/{corporation_id}/contracts/{contract_id}/items': { group: 'corp-contract', maxTokens: 600, windowSizeMs: 900000 },\n 'GET:corporations/{corporation_id}/customs_offices': { group: 'corp-industry', maxTokens: 600, windowSizeMs: 900000 },\n 'GET:corporations/{corporation_id}/divisions': { group: 'corp-wallet', maxTokens: 300, windowSizeMs: 900000 },\n 'GET:corporations/{corporation_id}/facilities': { group: 'corp-structure', maxTokens: 300, windowSizeMs: 900000 },\n 'GET:corporations/{corporation_id}/freelance-jobs': { group: 'corp-freelance-job', maxTokens: 300, windowSizeMs: 900000 },\n 'GET:corporations/{corporation_id}/freelance-jobs/{job_id}/participants': { group: 'corp-freelance-job', maxTokens: 300, windowSizeMs: 900000 },\n 'GET:corporations/{corporation_id}/fw/stats': { group: 'factional-warfare', maxTokens: 150, windowSizeMs: 900000 },\n 'GET:corporations/{corporation_id}/industry/jobs': { group: 'corp-industry', maxTokens: 600, windowSizeMs: 900000 },\n 'GET:corporations/{corporation_id}/killmails/recent': { group: 'corp-killmail', maxTokens: 30, windowSizeMs: 900000 },\n 'GET:corporations/{corporation_id}/medals': { group: 'corp-detail', maxTokens: 300, windowSizeMs: 900000 },\n 'GET:corporations/{corporation_id}/medals/issued': { group: 'corp-detail', maxTokens: 300, windowSizeMs: 900000 },\n 'GET:corporations/{corporation_id}/members': { group: 'corp-member', maxTokens: 300, windowSizeMs: 900000 },\n 'GET:corporations/{corporation_id}/members/limit': { group: 'corp-member', maxTokens: 300, windowSizeMs: 900000 },\n 'GET:corporations/{corporation_id}/members/titles': { group: 'corp-member', maxTokens: 300, windowSizeMs: 900000 },\n 'GET:corporations/{corporation_id}/membertracking': { group: 'corp-member', maxTokens: 300, windowSizeMs: 900000 },\n 'GET:corporations/{corporation_id}/projects': { group: 'corp-project', maxTokens: 600, windowSizeMs: 900000 },\n 'GET:corporations/{corporation_id}/projects/{project_id}': { group: 'corp-project', maxTokens: 600, windowSizeMs: 900000 },\n 'GET:corporations/{corporation_id}/projects/{project_id}/contribution/{character_id}': { group: 'corp-project', maxTokens: 600, windowSizeMs: 900000 },\n 'GET:corporations/{corporation_id}/projects/{project_id}/contributors': { group: 'corp-project', maxTokens: 600, windowSizeMs: 900000 },\n 'GET:corporations/{corporation_id}/roles': { group: 'corp-member', maxTokens: 300, windowSizeMs: 900000 },\n 'GET:corporations/{corporation_id}/roles/history': { group: 'corp-member', maxTokens: 300, windowSizeMs: 900000 },\n 'GET:corporations/{corporation_id}/shareholders': { group: 'corp-detail', maxTokens: 300, windowSizeMs: 900000 },\n 'GET:corporations/{corporation_id}/standings': { group: 'corp-member', maxTokens: 300, windowSizeMs: 900000 },\n 'GET:corporations/{corporation_id}/starbases': { group: 'corp-structure', maxTokens: 300, windowSizeMs: 900000 },\n 'GET:corporations/{corporation_id}/starbases/{starbase_id}': { group: 'corp-structure', maxTokens: 300, windowSizeMs: 900000 },\n 'GET:corporations/{corporation_id}/structures': { group: 'corp-structure', maxTokens: 300, windowSizeMs: 900000 },\n 'GET:corporations/{corporation_id}/structures/skyhooks': { group: 'corp-structure', maxTokens: 300, windowSizeMs: 900000 },\n 'GET:corporations/{corporation_id}/structures/skyhooks/{skyhook_id}': { group: 'corp-structure', maxTokens: 300, windowSizeMs: 900000 },\n 'GET:corporations/{corporation_id}/structures/sovereignty-hubs': { group: 'corp-structure', maxTokens: 300, windowSizeMs: 900000 },\n 'GET:corporations/{corporation_id}/structures/sovereignty-hubs/{sovereignty_hub_id}': { group: 'corp-structure', maxTokens: 300, windowSizeMs: 900000 },\n 'GET:corporations/{corporation_id}/titles': { group: 'corp-detail', maxTokens: 300, windowSizeMs: 900000 },\n 'GET:corporations/{corporation_id}/wallets': { group: 'corp-wallet', maxTokens: 300, windowSizeMs: 900000 },\n 'GET:corporations/{corporation_id}/wallets/{division}/journal': { group: 'corp-wallet', maxTokens: 300, windowSizeMs: 900000 },\n 'GET:corporations/{corporation_id}/wallets/{division}/transactions': { group: 'corp-wallet', maxTokens: 300, windowSizeMs: 900000 },\n 'GET:cosmetics/skinr/{skinr_id}': { group: 'skinr', maxTokens: 12000, windowSizeMs: 900000 },\n 'GET:fleets/{fleet_id}': { group: 'fleet', maxTokens: 1800, windowSizeMs: 900000 },\n 'PUT:fleets/{fleet_id}': { group: 'fleet', maxTokens: 1800, windowSizeMs: 900000 },\n 'GET:fleets/{fleet_id}/members': { group: 'fleet', maxTokens: 1800, windowSizeMs: 900000 },\n 'POST:fleets/{fleet_id}/members': { group: 'fleet', maxTokens: 1800, windowSizeMs: 900000 },\n 'PUT:fleets/{fleet_id}/members/{member_id}': { group: 'fleet', maxTokens: 1800, windowSizeMs: 900000 },\n 'DELETE:fleets/{fleet_id}/members/{member_id}': { group: 'fleet', maxTokens: 1800, windowSizeMs: 900000 },\n 'PUT:fleets/{fleet_id}/squads/{squad_id}': { group: 'fleet', maxTokens: 1800, windowSizeMs: 900000 },\n 'DELETE:fleets/{fleet_id}/squads/{squad_id}': { group: 'fleet', maxTokens: 1800, windowSizeMs: 900000 },\n 'GET:fleets/{fleet_id}/wings': { group: 'fleet', maxTokens: 1800, windowSizeMs: 900000 },\n 'POST:fleets/{fleet_id}/wings': { group: 'fleet', maxTokens: 1800, windowSizeMs: 900000 },\n 'PUT:fleets/{fleet_id}/wings/{wing_id}': { group: 'fleet', maxTokens: 1800, windowSizeMs: 900000 },\n 'DELETE:fleets/{fleet_id}/wings/{wing_id}': { group: 'fleet', maxTokens: 1800, windowSizeMs: 900000 },\n 'POST:fleets/{fleet_id}/wings/{wing_id}/squads': { group: 'fleet', maxTokens: 1800, windowSizeMs: 900000 },\n 'GET:freelance-jobs': { group: 'freelance-job', maxTokens: 12000, windowSizeMs: 900000 },\n 'GET:freelance-jobs/{job_id}': { group: 'freelance-job', maxTokens: 12000, windowSizeMs: 900000 },\n 'GET:fw/leaderboards': { group: 'factional-warfare', maxTokens: 150, windowSizeMs: 900000 },\n 'GET:fw/leaderboards/characters': { group: 'factional-warfare', maxTokens: 150, windowSizeMs: 900000 },\n 'GET:fw/leaderboards/corporations': { group: 'factional-warfare', maxTokens: 150, windowSizeMs: 900000 },\n 'GET:fw/stats': { group: 'factional-warfare', maxTokens: 150, windowSizeMs: 900000 },\n 'GET:fw/systems': { group: 'factional-warfare', maxTokens: 150, windowSizeMs: 900000 },\n 'GET:fw/wars': { group: 'factional-warfare', maxTokens: 150, windowSizeMs: 900000 },\n 'GET:incursions': { group: 'incursion', maxTokens: 150, windowSizeMs: 900000 },\n 'GET:industry/facilities': { group: 'industry', maxTokens: 150, windowSizeMs: 900000 },\n 'GET:industry/systems': { group: 'industry', maxTokens: 150, windowSizeMs: 900000 },\n 'GET:insurance/prices': { group: 'insurance', maxTokens: 150, windowSizeMs: 900000 },\n 'GET:killmails/{killmail_id}/{killmail_hash}': { group: 'killmail', maxTokens: 3600, windowSizeMs: 900000 },\n 'GET:markets/{region_id}/orders': { group: 'market-order', maxTokens: 12000, windowSizeMs: 900000 },\n 'GET:meta/changelog': { group: 'meta', maxTokens: 150, windowSizeMs: 900000 },\n 'GET:meta/compatibility-dates': { group: 'meta', maxTokens: 150, windowSizeMs: 900000 },\n 'GET:meta/name': { group: 'meta', maxTokens: 150, windowSizeMs: 900000 },\n 'GET:meta/status': { group: 'meta', maxTokens: 150, windowSizeMs: 900000 },\n 'GET:military-campaigns': { group: 'military-campaign', maxTokens: 300, windowSizeMs: 900000 },\n 'GET:military-campaigns/{campaign_id}': { group: 'military-campaign', maxTokens: 300, windowSizeMs: 900000 },\n 'GET:military-campaigns/{campaign_id}/objectives': { group: 'military-campaign', maxTokens: 300, windowSizeMs: 900000 },\n 'GET:military-campaigns/{campaign_id}/objectives/{objective_id}': { group: 'military-campaign', maxTokens: 300, windowSizeMs: 900000 },\n 'GET:paragon-hub/skinr': { group: 'paragon-hub', maxTokens: 150, windowSizeMs: 900000 },\n 'GET:paragon-hub/skinr/alliances/{alliance_id}': { group: 'char-paragon-hub', maxTokens: 150, windowSizeMs: 900000 },\n 'GET:paragon-hub/skinr/characters/{character_id}': { group: 'char-paragon-hub', maxTokens: 150, windowSizeMs: 900000 },\n 'GET:paragon-hub/skinr/corporations/{corporation_id}': { group: 'char-paragon-hub', maxTokens: 150, windowSizeMs: 900000 },\n 'POST:route/{origin_system_id}/{destination_system_id}': { group: 'routes', maxTokens: 3600, windowSizeMs: 900000 },\n 'GET:skyhooks/raidable': { group: 'activity', maxTokens: 30, windowSizeMs: 900000 },\n 'GET:sovereignty/campaigns': { group: 'sovereignty', maxTokens: 600, windowSizeMs: 900000 },\n 'GET:sovereignty/systems': { group: 'sovereignty', maxTokens: 600, windowSizeMs: 900000 },\n 'GET:status': { group: 'status', maxTokens: 600, windowSizeMs: 900000 },\n 'POST:ui/autopilot/waypoint': { group: 'ui', maxTokens: 900, windowSizeMs: 900000 },\n 'POST:ui/openwindow/contract': { group: 'ui', maxTokens: 900, windowSizeMs: 900000 },\n 'POST:ui/openwindow/information': { group: 'ui', maxTokens: 900, windowSizeMs: 900000 },\n 'POST:ui/openwindow/marketdetails': { group: 'ui', maxTokens: 900, windowSizeMs: 900000 },\n 'POST:ui/openwindow/newmail': { group: 'ui', maxTokens: 900, windowSizeMs: 900000 },\n 'GET:wars': { group: 'killmail', maxTokens: 3600, windowSizeMs: 900000 },\n 'GET:wars/{war_id}': { group: 'killmail', maxTokens: 3600, windowSizeMs: 900000 },\n 'GET:wars/{war_id}/killmails': { group: 'killmail', maxTokens: 3600, windowSizeMs: 900000 },\n};\n","import { IRateLimiter, RateLimitGroupStatus } from './IRateLimiter';\nimport {\n esiRateLimitGroups,\n RateLimitGroupSpec,\n} from '../endpoints/esi-rate-limit-groups.generated';\nimport { camelToSnake } from '../util/stringUtil';\nimport type { ApiClient } from '../ApiClient';\nimport { logWarn } from '../logger/clientLog';\nimport { sleep } from '../util/sleep';\nimport { EsiError } from '../util/error';\n\nexport interface RateLimitInfo {\n remaining: number;\n limit: number;\n used: number;\n group: string | null;\n errorLimitRemain: number;\n errorLimitReset: number;\n retryAfter: number | null;\n blockedUntil: number;\n}\n\nfunction getTokenCost(statusCode: number): number {\n if (statusCode >= 200 && statusCode < 300) return 2;\n if (statusCode >= 300 && statusCode < 400) return 1;\n if (statusCode >= 400 && statusCode < 500) return 5;\n if (statusCode >= 500 && statusCode < 600) return 0;\n return 2;\n}\n\n/**\n * When a Retry-After header lets requests resume, as epoch milliseconds.\n * RFC 9110 allows delay-seconds or an HTTP date. Undefined for anything else,\n * so an unreadable header never sets a block that cannot end.\n */\nfunction retryAfterDeadline(value: string, now: number): number | undefined {\n const trimmed = value.trim();\n if (/^\\d+$/.test(trimmed)) return now + parseInt(trimmed, 10) * 1000;\n if (!/[A-Za-z]/.test(trimmed)) return undefined;\n const date = Date.parse(trimmed);\n return Number.isNaN(date) ? undefined : date;\n}\n\nexport interface RateLimitEndpointOverride {\n maxTokens: number;\n windowSizeMs: number;\n}\n\nexport interface RateLimiterConfig {\n minDelayMs?: number | undefined;\n decelerationThreshold?: number | undefined;\n userKeyExtractor?: ((headers: Record<string, string>) => string) | undefined;\n endpointOverrides?: Record<string, RateLimitEndpointOverride> | undefined;\n}\n\ninterface GroupBucket {\n group: string;\n remaining: number;\n limit: number;\n used: number;\n windowSizeMs: number;\n blockedUntil: number;\n lastUpdated: number;\n}\n\ninterface ErrorLimitState {\n remain: number;\n reset: number;\n /** When the window of a budget reported as exhausted ends (epoch ms). */\n exhaustedUntil: number;\n}\n\ninterface UserBucketSet {\n groups: Map<string, GroupBucket>;\n errorLimit: ErrorLimitState;\n lastAccessed: number;\n}\n\nconst DEFAULT_WINDOW_MS = 900_000;\nconst DEFAULT_ERROR_LIMIT: ErrorLimitState = {\n remain: 100,\n reset: 0,\n exhaustedUntil: 0,\n};\n\nexport class RateLimiter implements IRateLimiter {\n private readonly minDelayMs: number;\n private readonly decelerationThreshold: number;\n private readonly userKeyExtractor?:\n ((headers: Record<string, string>) => string) | undefined;\n private readonly endpointOverrides: Record<string, RateLimitEndpointOverride>;\n\n private lastRequestTime: number = 0;\n private minDelayChain: Promise<void> = Promise.resolve();\n private isTestMode: boolean = false;\n\n private globalErrorLimit: ErrorLimitState = { ...DEFAULT_ERROR_LIMIT };\n\n private defaultBuckets: Map<string, GroupBucket> = new Map();\n private userBuckets: Map<string, UserBucketSet> | null = null;\n private fallbackBucket: GroupBucket;\n\n private lastCleanup: number = 0;\n private static readonly CLEANUP_INTERVAL_MS = 60_000;\n\n private client: ApiClient | null = null;\n\n /**\n * Attach the client whose logger this limiter's warnings go to. Without\n * one they go to the global logger (`setLogger`), then the pino default.\n */\n setClient(client: ApiClient | null): void {\n this.client = client;\n }\n\n constructor(config?: RateLimiterConfig) {\n this.minDelayMs = config?.minDelayMs ?? 50;\n this.decelerationThreshold = config?.decelerationThreshold ?? 0.2;\n this.userKeyExtractor = config?.userKeyExtractor;\n this.endpointOverrides = config?.endpointOverrides ?? {};\n if (this.userKeyExtractor) {\n this.userBuckets = new Map();\n }\n this.fallbackBucket = this.createFallbackBucket();\n }\n\n private createFallbackBucket(): GroupBucket {\n return {\n group: '__fallback__',\n remaining: -1,\n limit: 0,\n used: 0,\n windowSizeMs: DEFAULT_WINDOW_MS,\n blockedUntil: 0,\n lastUpdated: 0,\n };\n }\n\n private createBucketFromSpec(spec: RateLimitGroupSpec): GroupBucket {\n return {\n group: spec.group,\n remaining: spec.maxTokens,\n limit: spec.maxTokens,\n used: 0,\n windowSizeMs: spec.windowSizeMs,\n blockedUntil: 0,\n lastUpdated: 0,\n };\n }\n\n private createBucketForGroup(groupName: string): GroupBucket {\n return {\n group: groupName,\n remaining: -1,\n limit: 0,\n used: 0,\n windowSizeMs: DEFAULT_WINDOW_MS,\n blockedUntil: 0,\n lastUpdated: 0,\n };\n }\n\n private lookupGroupSpec(\n method?: string,\n templatePath?: string,\n ): RateLimitGroupSpec | undefined {\n if (!templatePath || !method) return undefined;\n const normalized = templatePath\n .replace(/\\/$/, '')\n .replace(/\\{(\\w+)\\}/g, (_, name: string) => `{${camelToSnake(name)}}`);\n const key = `${method}:${normalized}`;\n\n const override = this.endpointOverrides[key];\n if (override) {\n return {\n group: `endpoint:${key}`,\n maxTokens: override.maxTokens,\n windowSizeMs: override.windowSizeMs,\n };\n }\n\n return esiRateLimitGroups[key];\n }\n\n private resolveUserSet(\n requestHeaders?: Record<string, string>,\n ): UserBucketSet | null {\n if (!this.userBuckets || !this.userKeyExtractor || !requestHeaders) {\n return null;\n }\n\n const userKey = this.userKeyExtractor(requestHeaders);\n let userSet = this.userBuckets.get(userKey);\n if (!userSet) {\n userSet = {\n groups: new Map(),\n errorLimit: { ...DEFAULT_ERROR_LIMIT },\n lastAccessed: Date.now(),\n };\n this.userBuckets.set(userKey, userSet);\n }\n userSet.lastAccessed = Date.now();\n return userSet;\n }\n\n private resolveBucketMap(\n requestHeaders?: Record<string, string>,\n ): Map<string, GroupBucket> {\n const userSet = this.resolveUserSet(requestHeaders);\n return userSet ? userSet.groups : this.defaultBuckets;\n }\n\n private resolveErrorLimit(\n requestHeaders?: Record<string, string>,\n ): ErrorLimitState {\n const userSet = this.resolveUserSet(requestHeaders);\n return userSet ? userSet.errorLimit : this.globalErrorLimit;\n }\n\n private getBucket(\n templatePath?: string,\n method?: string,\n requestHeaders?: Record<string, string>,\n groupNameOverride?: string,\n ): GroupBucket {\n const bucketMap = this.resolveBucketMap(requestHeaders);\n\n const spec = this.lookupGroupSpec(method, templatePath);\n const groupName = groupNameOverride ?? spec?.group;\n\n if (!groupName) return this.fallbackBucket;\n\n let bucket = bucketMap.get(groupName);\n if (!bucket) {\n bucket = spec\n ? this.createBucketFromSpec(spec)\n : this.createBucketForGroup(groupName);\n bucketMap.set(groupName, bucket);\n }\n return bucket;\n }\n\n private cleanupStaleUsers(): void {\n if (!this.userBuckets) return;\n const now = Date.now();\n if (now - this.lastCleanup < RateLimiter.CLEANUP_INTERVAL_MS) return;\n this.lastCleanup = now;\n\n for (const [userKey, userSet] of this.userBuckets) {\n if (now - userSet.lastAccessed > DEFAULT_WINDOW_MS) {\n this.userBuckets.delete(userKey);\n }\n }\n }\n\n updateFromResponse(\n headers: Record<string, string>,\n statusCode: number,\n templatePath?: string,\n method?: string,\n requestHeaders?: Record<string, string>,\n ): void {\n if (this.isTestMode) return;\n\n const now = Date.now();\n const responseGroup = headers['x-ratelimit-group'] || undefined;\n const bucket = this.getBucket(\n templatePath,\n method,\n requestHeaders,\n responseGroup,\n );\n\n if ('x-ratelimit-remaining' in headers) {\n bucket.remaining = parseInt(headers['x-ratelimit-remaining'], 10);\n }\n if ('x-ratelimit-limit' in headers) {\n bucket.limit = parseInt(headers['x-ratelimit-limit'], 10);\n }\n if ('x-ratelimit-used' in headers) {\n bucket.used = parseInt(headers['x-ratelimit-used'], 10);\n }\n bucket.lastUpdated = now;\n\n if ('retry-after' in headers) {\n const until = retryAfterDeadline(headers['retry-after'], now);\n if (until !== undefined) bucket.blockedUntil = until;\n }\n\n if (statusCode === 420 || statusCode === 429) {\n if (bucket.blockedUntil <= now) {\n bucket.blockedUntil = now + 60_000;\n }\n }\n\n const errorLimit = this.resolveErrorLimit(requestHeaders);\n const reportedRemain =\n 'x-esi-error-limit-remain' in headers\n ? parseInt(headers['x-esi-error-limit-remain'], 10)\n : undefined;\n // Inside the window of a budget already reported as exhausted, a response\n // reporting budget left was processed before the one that exhausted it\n // and arrived late. Believing it would lift the back-off for every\n // endpoint while ESI still counts errors against the spent budget.\n if (\n reportedRemain !== undefined &&\n reportedRemain > 0 &&\n errorLimit.remain <= 0 &&\n now < errorLimit.exhaustedUntil\n ) {\n return;\n }\n if (reportedRemain !== undefined) {\n errorLimit.remain = reportedRemain;\n }\n if ('x-esi-error-limit-reset' in headers) {\n errorLimit.reset = parseInt(headers['x-esi-error-limit-reset'], 10);\n }\n if (reportedRemain !== undefined && reportedRemain <= 0) {\n errorLimit.exhaustedUntil = now + errorLimit.reset * 1000;\n }\n }\n\n updateRateLimitInfo(headers: Record<string, string>): void {\n this.updateFromResponse(headers, 200);\n }\n\n async checkRateLimit(\n templatePath?: string,\n method?: string,\n requestHeaders?: Record<string, string>,\n ): Promise<void> {\n if (this.isTestMode) return;\n\n this.cleanupStaleUsers();\n\n // Re-check after sleeping on a block so a concurrent response that\n // re-blocked the group is respected (bounded to avoid infinite waits).\n let stillBlockedBucket: GroupBucket | undefined;\n for (let i = 0; i < 10; i++) {\n const bucket = this.getBucket(templatePath, method, requestHeaders);\n const now = Date.now();\n if (bucket.blockedUntil <= now) {\n stillBlockedBucket = undefined;\n break;\n }\n stillBlockedBucket = bucket;\n const waitedUntil = bucket.blockedUntil;\n const waitTime = waitedUntil - now;\n logWarn(\n this.client,\n `[ESI Rate Limit] Group '${bucket.group}' blocked for ${Math.ceil(waitTime / 1000)}s (420/429 received)`,\n { group: bucket.group, waitMs: waitTime },\n );\n await sleep(waitTime);\n // Clear the block we waited for; leave intact if another response extended it\n if (bucket.blockedUntil <= waitedUntil) {\n bucket.blockedUntil = 0;\n }\n }\n // Never proceed while still blocked: if concurrent responses kept\n // re-extending blockedUntil past the retry budget above, fail fast\n // with a retryable rate-limit error rather than sending the request.\n if (stillBlockedBucket && stillBlockedBucket.blockedUntil > Date.now()) {\n const waitTime = stillBlockedBucket.blockedUntil - Date.now();\n logWarn(\n this.client,\n `[ESI Rate Limit] Group '${stillBlockedBucket.group}' still blocked after retry budget exhausted; aborting request instead of sending while blocked`,\n { group: stillBlockedBucket.group },\n );\n throw new EsiError(\n 429,\n `Rate limit group '${stillBlockedBucket.group}' still blocked for ${Math.ceil(waitTime / 1000)}s`,\n );\n }\n\n const errorLimit = this.resolveErrorLimit(requestHeaders);\n if (errorLimit.remain <= 10 && errorLimit.remain > 0) {\n const resetMs = errorLimit.reset * 1000;\n const delay = Math.min(resetMs, 5000);\n if (delay > 0) {\n logWarn(\n this.client,\n `[ESI Rate Limit] Legacy error limit low (${errorLimit.remain}), waiting ${delay}ms`,\n { errorLimitRemain: errorLimit.remain },\n );\n await sleep(delay);\n }\n } else if (errorLimit.remain <= 0 && errorLimit.reset > 0) {\n const waitTime = errorLimit.reset * 1000;\n logWarn(\n this.client,\n `[ESI Rate Limit] Legacy error limit exhausted, waiting ${Math.ceil(waitTime / 1000)}s`,\n { errorLimitReset: errorLimit.reset },\n );\n await sleep(waitTime);\n }\n\n const bucket = this.getBucket(templatePath, method, requestHeaders);\n if (bucket.remaining >= 0 && bucket.limit > 0) {\n const ratio = bucket.remaining / bucket.limit;\n\n if (bucket.remaining <= 0) {\n logWarn(\n this.client,\n `[ESI Rate Limit] Group '${bucket.group}' token bucket empty, waiting 1s`,\n { group: bucket.group },\n );\n await sleep(1000);\n } else if (ratio <= this.decelerationThreshold) {\n const extraDelay = Math.ceil(\n (1 - ratio / this.decelerationThreshold) * 1000,\n );\n await sleep(extraDelay);\n }\n }\n\n await this.enforceMinDelay();\n }\n\n /**\n * Serializes min-delay waits so concurrent callers cannot all read the same\n * lastRequestTime and fire together after sleeping.\n */\n private async enforceMinDelay(): Promise<void> {\n if (this.isTestMode) {\n this.lastRequestTime = Date.now();\n return;\n }\n\n const run = async (): Promise<void> => {\n const now = Date.now();\n const timeSinceLastRequest = now - this.lastRequestTime;\n if (timeSinceLastRequest < this.minDelayMs) {\n await sleep(this.minDelayMs - timeSinceLastRequest);\n }\n this.lastRequestTime = Date.now();\n };\n\n const scheduled = this.minDelayChain.then(run, run);\n this.minDelayChain = scheduled.then(\n () => undefined,\n () => undefined,\n );\n await scheduled;\n }\n\n getStatus(): RateLimitInfo {\n let worstBucket = this.fallbackBucket;\n let worstRatio = Infinity;\n\n const checkBucket = (bucket: GroupBucket): void => {\n if (bucket.limit > 0) {\n const ratio = bucket.remaining / bucket.limit;\n if (ratio < worstRatio) {\n worstRatio = ratio;\n worstBucket = bucket;\n }\n }\n };\n\n for (const bucket of this.defaultBuckets.values()) {\n checkBucket(bucket);\n }\n\n if (this.userBuckets) {\n for (const userSet of this.userBuckets.values()) {\n for (const bucket of userSet.groups.values()) {\n checkBucket(bucket);\n }\n }\n }\n\n return {\n remaining: worstBucket.remaining,\n limit: worstBucket.limit,\n used: worstBucket.used,\n group: worstBucket.group === '__fallback__' ? null : worstBucket.group,\n errorLimitRemain: this.globalErrorLimit.remain,\n errorLimitReset: this.globalErrorLimit.reset,\n retryAfter:\n worstBucket.blockedUntil > Date.now()\n ? Math.ceil((worstBucket.blockedUntil - Date.now()) / 1000)\n : null,\n blockedUntil: worstBucket.blockedUntil,\n };\n }\n\n getGroupStatus(group: string): RateLimitGroupStatus | undefined {\n const bucket = this.defaultBuckets.get(group);\n if (!bucket) return undefined;\n return {\n group: bucket.group,\n remaining: bucket.remaining,\n limit: bucket.limit,\n used: bucket.used,\n windowSizeMs: bucket.windowSizeMs,\n blockedUntil: bucket.blockedUntil,\n };\n }\n\n getAllGroupStatuses(): Map<string, RateLimitGroupStatus> {\n const result = new Map<string, RateLimitGroupStatus>();\n for (const [name, bucket] of this.defaultBuckets) {\n result.set(name, {\n group: bucket.group,\n remaining: bucket.remaining,\n limit: bucket.limit,\n used: bucket.used,\n windowSizeMs: bucket.windowSizeMs,\n blockedUntil: bucket.blockedUntil,\n });\n }\n return result;\n }\n\n static getTokenCost(statusCode: number): number {\n return getTokenCost(statusCode);\n }\n\n isBlocked(group?: string): boolean {\n const now = Date.now();\n\n if (group) {\n const bucket = this.defaultBuckets.get(group);\n return bucket ? bucket.blockedUntil > now : false;\n }\n\n if (this.fallbackBucket.blockedUntil > now) return true;\n for (const bucket of this.defaultBuckets.values()) {\n if (bucket.blockedUntil > now) return true;\n }\n return false;\n }\n\n setTestMode(enabled: boolean): void {\n this.isTestMode = enabled;\n }\n\n reset(): void {\n this.defaultBuckets.clear();\n if (this.userBuckets) this.userBuckets.clear();\n this.fallbackBucket = this.createFallbackBucket();\n this.globalErrorLimit = { ...DEFAULT_ERROR_LIMIT };\n this.lastRequestTime = 0;\n this.minDelayChain = Promise.resolve();\n this.lastCleanup = 0;\n }\n}\n","import { logDebug } from './logger/clientLog';\nimport { IDeduplicator } from './IDeduplicator';\n\nexport class RequestDeduplicator implements IDeduplicator {\n private inflight = new Map<string, Promise<unknown>>();\n private client: import('./ApiClient').ApiClient | null = null;\n\n setClient(client: import('./ApiClient').ApiClient | null): void {\n this.client = client;\n }\n\n async dedupe<T>(key: string, execute: () => Promise<T>): Promise<T> {\n const existing = this.inflight.get(key);\n if (existing) {\n if (this.client) {\n logDebug(this.client, `[Dedup] Coalescing request: ${key}`);\n }\n return existing as Promise<T>;\n }\n\n const promise = execute().finally(() => {\n this.inflight.delete(key);\n });\n\n this.inflight.set(key, promise);\n return promise;\n }\n\n /**\n * Drop the in-flight entries whose key contains `pathSegment`, so a call\n * made after a write cannot join a request sent before it.\n *\n * The requests themselves keep running and whoever already joined them still\n * gets their answer. Only the map entry goes, which is what new callers look\n * in. Matching is `includes`, the same rule ETagCacheManager.deleteByPath\n * uses, so one written path invalidates the cache and detaches the reads of\n * it by the same test.\n */\n detachByPath(pathSegment: string): number {\n let count = 0;\n for (const key of this.inflight.keys()) {\n if (key.includes(pathSegment)) {\n this.inflight.delete(key);\n count += 1;\n }\n }\n if (count > 0 && this.client) {\n logDebug(\n this.client,\n `[Dedup] Detached ${count} in-flight request(s) matching ${pathSegment} after a write`,\n );\n }\n return count;\n }\n\n get pending(): number {\n return this.inflight.size;\n }\n\n clear(): void {\n this.inflight.clear();\n }\n}\n","import { ApiClient } from './ApiClient';\nimport { ETagCacheManager } from './cache/ETagCacheManager';\nimport { CircuitBreaker } from './circuitBreaker/CircuitBreaker';\nimport { RateLimiter } from './rateLimiter/RateLimiter';\nimport { RequestDeduplicator } from './RequestDeduplicator';\nimport { createDefaultLogger } from './logger/DefaultLogger';\nimport type { EsiClientConfig } from './EsiClientConfig';\n\nexport interface ConfigureApiClientResult {\n deduplicator: RequestDeduplicator | null;\n}\n\n/**\n * Configures an existing ApiClient with the full middleware stack\n * based on an EsiClientConfig. This is the single source of truth\n * for how config maps to middleware — used by EsiClient,\n * CustomEsiClient, and EsiApiFactory.\n *\n * The caller is responsible for creating the ApiClient (with baseUrl,\n * clientId, accessToken) and setting datasource/language/tokenProvider\n * before calling this function, since those vary by construction surface.\n */\nexport function configureApiClient(\n client: ApiClient,\n config?: EsiClientConfig,\n): ConfigureApiClientResult {\n let deduplicator: RequestDeduplicator | null = null;\n\n // Per-client logger first, so the middleware built below (the ETag\n // cache's startup line included) logs to it from the start.\n // Only when explicitly configured. Otherwise the pipeline\n // resolves to the global setLogger() logger, then the built-in pino default\n // (level from ESI_LOG_LEVEL), so existing global-logger setups keep working.\n if (config?.logger) {\n client.setLogger(config.logger);\n } else if (config?.logLevel) {\n client.setLogger(createDefaultLogger(config.logLevel));\n }\n\n // Per-client identity headers. The setters refuse a value that is not a\n // valid header, so a bad one fails here rather than on every request.\n if (config?.tenant !== undefined) client.setTenant(config.tenant);\n if (config?.userAgent !== undefined) client.setUserAgent(config.userAgent);\n\n // Rate limiter (always)\n const rateLimiter = new RateLimiter(config?.rateLimiterConfig);\n rateLimiter.setClient(client);\n client.setRateLimiter(rateLimiter);\n\n // Request deduplication (on by default)\n if (config?.enableRequestDeduplication !== false) {\n deduplicator = new RequestDeduplicator();\n deduplicator.setClient(client);\n client.setDeduplicator(deduplicator);\n }\n\n // ETag cache (on by default)\n if (config?.enableETagCache !== false) {\n const cache = new ETagCacheManager(config?.etagCacheConfig, client);\n client.setCache(cache);\n }\n\n // Circuit breaker (off by default, opt-in)\n if (config?.enableCircuitBreaker) {\n const cb = new CircuitBreaker(config.circuitBreakerConfig);\n cb.setClient(client);\n client.setCircuitBreaker(cb);\n }\n\n // Retry config — default to 3 retries with exponential backoff so that\n // transient ESI failures (502/503/504) are handled automatically.\n if (config?.retryConfig) {\n client.setRetryConfig(config.retryConfig);\n } else if (config?.retryAttempts !== undefined) {\n client.setRetryConfig({ maxRetries: config.retryAttempts });\n } else {\n client.setRetryConfig({\n maxRetries: 3,\n baseDelayMs: 1000,\n maxDelayMs: 30000,\n });\n }\n\n // Retry strategy\n if (config?.retryStrategy) {\n client.setRetryStrategy(config.retryStrategy);\n }\n\n // Interceptors\n if (config?.requestInterceptors) {\n for (const interceptor of config.requestInterceptors) {\n client.addRequestInterceptor(interceptor);\n }\n }\n if (config?.responseInterceptors) {\n for (const interceptor of config.responseInterceptors) {\n client.addResponseInterceptor(interceptor);\n }\n }\n\n // Validate response\n if (config?.validateResponse !== undefined) {\n client.setValidateResponse(config.validateResponse);\n }\n\n // Validate request (opt-in, default false)\n if (config?.validateRequest !== undefined) {\n client.setValidateRequest(config.validateRequest);\n }\n\n // Timeout\n if (config?.timeout !== undefined) {\n client.setTimeout(config.timeout);\n }\n\n return { deduplicator };\n}\n"]}
|
|
@@ -49,7 +49,19 @@ function isSdeVersionMismatch(error) {
|
|
|
49
49
|
return error instanceof SdeVersionMismatchError;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
// src/sde/
|
|
52
|
+
// src/sde/providers/order.ts
|
|
53
|
+
function compareIds(a, b) {
|
|
54
|
+
if (typeof a === "number" && typeof b === "number") return a - b;
|
|
55
|
+
if (typeof a === "number") return -1;
|
|
56
|
+
if (typeof b === "number") return 1;
|
|
57
|
+
if (a < b) return -1;
|
|
58
|
+
return a > b ? 1 : 0;
|
|
59
|
+
}
|
|
60
|
+
function sortedById(items, idOf) {
|
|
61
|
+
return [...items].sort((x, y) => compareIds(idOf(x), idOf(y)));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// src/sde/providers/memory/MemorySdeProvider.ts
|
|
53
65
|
var MemorySdeProvider = (_class = class {
|
|
54
66
|
__init() {this.data = /* @__PURE__ */ new Map()}
|
|
55
67
|
|
|
@@ -57,7 +69,9 @@ var MemorySdeProvider = (_class = class {
|
|
|
57
69
|
const register = (key, items, idFn) => {
|
|
58
70
|
this.data.set(
|
|
59
71
|
key,
|
|
60
|
-
new Map(
|
|
72
|
+
new Map(
|
|
73
|
+
sortedById(_nullishCoalesce(items, () => ( [])), idFn).map((item) => [idFn(item), item])
|
|
74
|
+
)
|
|
61
75
|
);
|
|
62
76
|
};
|
|
63
77
|
register("eve_types", data.types, (t) => t.typeId);
|
|
@@ -529,7 +543,7 @@ var MemorySdeProvider = (_class = class {
|
|
|
529
543
|
}
|
|
530
544
|
}, _class);
|
|
531
545
|
|
|
532
|
-
// src/sde/SdeTestDataFactory.ts
|
|
546
|
+
// src/sde/testing/SdeTestDataFactory.ts
|
|
533
547
|
var SdeTestDataFactory = class {
|
|
534
548
|
static createEveType(overrides = {}) {
|
|
535
549
|
return {
|
|
@@ -1061,5 +1075,6 @@ var SdeTestDataFactory = class {
|
|
|
1061
1075
|
|
|
1062
1076
|
|
|
1063
1077
|
|
|
1064
|
-
|
|
1065
|
-
|
|
1078
|
+
|
|
1079
|
+
exports.sortedById = sortedById; exports.SdeError = SdeError; exports.SdeDatabaseError = SdeDatabaseError; exports.SdeValidationError = SdeValidationError; exports.SdeVersionMismatchError = SdeVersionMismatchError; exports.isSdeError = isSdeError; exports.isSdeDatabaseError = isSdeDatabaseError; exports.isSdeValidationError = isSdeValidationError; exports.isSdeVersionMismatch = isSdeVersionMismatch; exports.MemorySdeProvider = MemorySdeProvider; exports.SdeTestDataFactory = SdeTestDataFactory;
|
|
1080
|
+
//# sourceMappingURL=chunk-S73PKM2Y.js.map
|