@lgriffin/esi.ts 6.1.0 → 8.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.
Files changed (426) hide show
  1. package/CHANGELOG.md +84 -1
  2. package/LICENSE +26 -26
  3. package/README.md +967 -896
  4. package/dist/EsiClient.d.ts +3 -0
  5. package/dist/EsiClient.d.ts.map +1 -1
  6. package/dist/EsiClientBuilder.d.ts +1 -0
  7. package/dist/EsiClientBuilder.d.ts.map +1 -1
  8. package/dist/clients/AllianceClient.d.ts +5 -0
  9. package/dist/clients/AllianceClient.d.ts.map +1 -1
  10. package/dist/clients/BaseEsiClient.d.ts +5 -3
  11. package/dist/clients/BaseEsiClient.d.ts.map +1 -1
  12. package/dist/clients/CalendarClient.d.ts +3 -0
  13. package/dist/clients/CalendarClient.d.ts.map +1 -1
  14. package/dist/clients/CharacterClient.d.ts +10 -2
  15. package/dist/clients/CharacterClient.d.ts.map +1 -1
  16. package/dist/clients/ClonesClient.d.ts +2 -0
  17. package/dist/clients/ClonesClient.d.ts.map +1 -1
  18. package/dist/clients/ContactsClient.d.ts +7 -0
  19. package/dist/clients/ContactsClient.d.ts.map +1 -1
  20. package/dist/clients/CorporationsClient.d.ts +18 -0
  21. package/dist/clients/CorporationsClient.d.ts.map +1 -1
  22. package/dist/clients/FittingsClient.d.ts +2 -0
  23. package/dist/clients/FittingsClient.d.ts.map +1 -1
  24. package/dist/clients/FleetClient.d.ts +3 -0
  25. package/dist/clients/FleetClient.d.ts.map +1 -1
  26. package/dist/clients/IndustryClient.d.ts +9 -0
  27. package/dist/clients/IndustryClient.d.ts.map +1 -1
  28. package/dist/clients/LoyaltyClient.d.ts +3 -0
  29. package/dist/clients/LoyaltyClient.d.ts.map +1 -1
  30. package/dist/clients/MailClient.d.ts +6 -0
  31. package/dist/clients/MailClient.d.ts.map +1 -1
  32. package/dist/clients/MarketClient.d.ts +13 -22
  33. package/dist/clients/MarketClient.d.ts.map +1 -1
  34. package/dist/clients/PiClient.d.ts +3 -0
  35. package/dist/clients/PiClient.d.ts.map +1 -1
  36. package/dist/clients/SkillsClient.d.ts +2 -0
  37. package/dist/clients/SkillsClient.d.ts.map +1 -1
  38. package/dist/clients/WalletClient.d.ts +1 -0
  39. package/dist/clients/WalletClient.d.ts.map +1 -1
  40. package/dist/clients/WarsClient.d.ts +3 -0
  41. package/dist/clients/WarsClient.d.ts.map +1 -1
  42. package/dist/core/ApiClient.d.ts +14 -6
  43. package/dist/core/ApiClient.d.ts.map +1 -1
  44. package/dist/core/ApiRequestHandler.d.ts +3 -11
  45. package/dist/core/ApiRequestHandler.d.ts.map +1 -1
  46. package/dist/core/IDeduplicator.d.ts +6 -0
  47. package/dist/core/IDeduplicator.d.ts.map +1 -0
  48. package/dist/core/IRetryStrategy.d.ts +5 -0
  49. package/dist/core/IRetryStrategy.d.ts.map +1 -0
  50. package/dist/core/RequestDeduplicator.d.ts +2 -1
  51. package/dist/core/RequestDeduplicator.d.ts.map +1 -1
  52. package/dist/core/RetryStrategy.d.ts +19 -0
  53. package/dist/core/RetryStrategy.d.ts.map +1 -0
  54. package/dist/core/circuitBreaker/CircuitBreaker.d.ts +11 -1
  55. package/dist/core/circuitBreaker/CircuitBreaker.d.ts.map +1 -1
  56. package/dist/core/circuitBreaker/ICircuitBreaker.d.ts +23 -0
  57. package/dist/core/circuitBreaker/ICircuitBreaker.d.ts.map +1 -0
  58. package/dist/core/configureApiClient.d.ts +18 -0
  59. package/dist/core/configureApiClient.d.ts.map +1 -0
  60. package/dist/core/constants.d.ts +2 -2
  61. package/dist/core/endpoints/EndpointDefinition.d.ts +2 -0
  62. package/dist/core/endpoints/EndpointDefinition.d.ts.map +1 -1
  63. package/dist/core/endpoints/allianceEndpoints.d.ts +17 -14
  64. package/dist/core/endpoints/allianceEndpoints.d.ts.map +1 -1
  65. package/dist/core/endpoints/assetEndpoints.d.ts +15 -0
  66. package/dist/core/endpoints/assetEndpoints.d.ts.map +1 -1
  67. package/dist/core/endpoints/calendarEndpoints.d.ts +1 -1
  68. package/dist/core/endpoints/characterEndpoints.d.ts +11 -3
  69. package/dist/core/endpoints/characterEndpoints.d.ts.map +1 -1
  70. package/dist/core/endpoints/cloneEndpoints.d.ts +17 -15
  71. package/dist/core/endpoints/cloneEndpoints.d.ts.map +1 -1
  72. package/dist/core/endpoints/contactEndpoints.d.ts +7 -3
  73. package/dist/core/endpoints/contactEndpoints.d.ts.map +1 -1
  74. package/dist/core/endpoints/corporationEndpoints.d.ts +30 -0
  75. package/dist/core/endpoints/corporationEndpoints.d.ts.map +1 -1
  76. package/dist/core/endpoints/createClient.d.ts +23 -2
  77. package/dist/core/endpoints/createClient.d.ts.map +1 -1
  78. package/dist/core/endpoints/dogmaEndpoints.d.ts +58 -38
  79. package/dist/core/endpoints/dogmaEndpoints.d.ts.map +1 -1
  80. package/dist/core/endpoints/esi-cache-ttls.generated.d.ts.map +1 -1
  81. package/dist/core/endpoints/esi-scopes.generated.d.ts +1 -1
  82. package/dist/core/endpoints/esi-scopes.generated.d.ts.map +1 -1
  83. package/dist/core/endpoints/fleetEndpoints.d.ts +5 -0
  84. package/dist/core/endpoints/fleetEndpoints.d.ts.map +1 -1
  85. package/dist/core/endpoints/mailEndpoints.d.ts +26 -0
  86. package/dist/core/endpoints/mailEndpoints.d.ts.map +1 -1
  87. package/dist/core/endpoints/marketEndpoints.d.ts +44 -43
  88. package/dist/core/endpoints/marketEndpoints.d.ts.map +1 -1
  89. package/dist/core/endpoints/metaEndpoints.d.ts +2 -0
  90. package/dist/core/endpoints/metaEndpoints.d.ts.map +1 -1
  91. package/dist/core/endpoints/piEndpoints.d.ts +7 -0
  92. package/dist/core/endpoints/piEndpoints.d.ts.map +1 -1
  93. package/dist/core/endpoints/routeEndpoints.d.ts +4 -0
  94. package/dist/core/endpoints/routeEndpoints.d.ts.map +1 -1
  95. package/dist/core/endpoints/searchEndpoints.d.ts +13 -0
  96. package/dist/core/endpoints/searchEndpoints.d.ts.map +1 -1
  97. package/dist/core/endpoints/skillEndpoints.d.ts +10 -0
  98. package/dist/core/endpoints/skillEndpoints.d.ts.map +1 -1
  99. package/dist/core/endpoints/universeEndpoints.d.ts +21 -2
  100. package/dist/core/endpoints/universeEndpoints.d.ts.map +1 -1
  101. package/dist/core/endpoints/walletEndpoints.d.ts +9 -0
  102. package/dist/core/endpoints/walletEndpoints.d.ts.map +1 -1
  103. package/dist/core/endpoints/warEndpoints.d.ts +1 -0
  104. package/dist/core/endpoints/warEndpoints.d.ts.map +1 -1
  105. package/dist/core/logger/logger.d.ts +2 -1
  106. package/dist/core/logger/logger.d.ts.map +1 -1
  107. package/dist/core/pagination/AsyncPaginationIterator.d.ts +4 -1
  108. package/dist/core/pagination/AsyncPaginationIterator.d.ts.map +1 -1
  109. package/dist/core/pagination/CursorPaginationHandler.d.ts +6 -2
  110. package/dist/core/pagination/CursorPaginationHandler.d.ts.map +1 -1
  111. package/dist/core/pagination/PaginationHandler.d.ts +1 -1
  112. package/dist/core/pagination/PaginationHandler.d.ts.map +1 -1
  113. package/dist/core/rateLimiter/RateLimiter.d.ts +8 -2
  114. package/dist/core/rateLimiter/RateLimiter.d.ts.map +1 -1
  115. package/dist/core/requestPipeline/cachePolicy.d.ts +30 -0
  116. package/dist/core/requestPipeline/cachePolicy.d.ts.map +1 -0
  117. package/dist/core/requestPipeline/dependencies.d.ts +10 -0
  118. package/dist/core/requestPipeline/dependencies.d.ts.map +1 -0
  119. package/dist/core/requestPipeline/fetchExecution.d.ts +28 -0
  120. package/dist/core/requestPipeline/fetchExecution.d.ts.map +1 -0
  121. package/dist/core/requestPipeline/headers.d.ts +11 -0
  122. package/dist/core/requestPipeline/headers.d.ts.map +1 -0
  123. package/dist/core/requestPipeline/index.d.ts +10 -0
  124. package/dist/core/requestPipeline/index.d.ts.map +1 -0
  125. package/dist/core/requestPipeline/middlewareBridge.d.ts +15 -0
  126. package/dist/core/requestPipeline/middlewareBridge.d.ts.map +1 -0
  127. package/dist/core/requestPipeline/paginationOrchestration.d.ts +15 -0
  128. package/dist/core/requestPipeline/paginationOrchestration.d.ts.map +1 -0
  129. package/dist/core/requestPipeline/statusHandling.d.ts +18 -0
  130. package/dist/core/requestPipeline/statusHandling.d.ts.map +1 -0
  131. package/dist/core/util/error.d.ts +4 -1
  132. package/dist/core/util/error.d.ts.map +1 -1
  133. package/dist/index.d.ts +9 -2
  134. package/dist/index.d.ts.map +1 -1
  135. package/dist/index.js +9926 -176
  136. package/dist/index.js.map +1 -0
  137. package/dist/index.mjs +9824 -0
  138. package/dist/index.mjs.map +1 -0
  139. package/dist/schemas/calendar.d.ts +1 -1
  140. package/dist/schemas/character.d.ts +9 -10
  141. package/dist/schemas/character.d.ts.map +1 -1
  142. package/dist/schemas/common.d.ts +9 -0
  143. package/dist/schemas/common.d.ts.map +1 -1
  144. package/dist/schemas/contacts.d.ts +2 -1
  145. package/dist/schemas/contacts.d.ts.map +1 -1
  146. package/dist/schemas/corporation.d.ts +13 -0
  147. package/dist/schemas/corporation.d.ts.map +1 -1
  148. package/dist/schemas/dogma.d.ts +17 -0
  149. package/dist/schemas/dogma.d.ts.map +1 -1
  150. package/dist/schemas/fleet.d.ts +1 -0
  151. package/dist/schemas/fleet.d.ts.map +1 -1
  152. package/dist/schemas/generated/alliance.generated.d.ts +15 -0
  153. package/dist/schemas/generated/alliance.generated.d.ts.map +1 -0
  154. package/dist/schemas/generated/assets.generated.d.ts +272 -0
  155. package/dist/schemas/generated/assets.generated.d.ts.map +1 -0
  156. package/dist/schemas/generated/calendar.generated.d.ts +41 -0
  157. package/dist/schemas/generated/calendar.generated.d.ts.map +1 -0
  158. package/dist/schemas/generated/character.generated.d.ts +670 -0
  159. package/dist/schemas/generated/character.generated.d.ts.map +1 -0
  160. package/dist/schemas/generated/clones.generated.d.ts +23 -0
  161. package/dist/schemas/generated/clones.generated.d.ts.map +1 -0
  162. package/dist/schemas/generated/contacts.generated.d.ts +50 -0
  163. package/dist/schemas/generated/contacts.generated.d.ts.map +1 -0
  164. package/dist/schemas/generated/contracts.generated.d.ts +162 -0
  165. package/dist/schemas/generated/contracts.generated.d.ts.map +1 -0
  166. package/dist/schemas/generated/corporation-projects.generated.d.ts +89 -0
  167. package/dist/schemas/generated/corporation-projects.generated.d.ts.map +1 -0
  168. package/dist/schemas/generated/corporation.generated.d.ts +1515 -0
  169. package/dist/schemas/generated/corporation.generated.d.ts.map +1 -0
  170. package/dist/schemas/generated/dogma.generated.d.ts +57 -0
  171. package/dist/schemas/generated/dogma.generated.d.ts.map +1 -0
  172. package/dist/schemas/generated/faction-warfare.generated.d.ts +155 -0
  173. package/dist/schemas/generated/faction-warfare.generated.d.ts.map +1 -0
  174. package/dist/schemas/generated/fittings.generated.d.ts +57 -0
  175. package/dist/schemas/generated/fittings.generated.d.ts.map +1 -0
  176. package/dist/schemas/generated/fleets.generated.d.ts +45 -0
  177. package/dist/schemas/generated/fleets.generated.d.ts.map +1 -0
  178. package/dist/schemas/generated/freelance-jobs.generated.d.ts +173 -0
  179. package/dist/schemas/generated/freelance-jobs.generated.d.ts.map +1 -0
  180. package/dist/schemas/generated/incursions.generated.d.ts +16 -0
  181. package/dist/schemas/generated/incursions.generated.d.ts.map +1 -0
  182. package/dist/schemas/generated/index.d.ts +33 -0
  183. package/dist/schemas/generated/index.d.ts.map +1 -0
  184. package/dist/schemas/generated/industry.generated.d.ts +117 -0
  185. package/dist/schemas/generated/industry.generated.d.ts.map +1 -0
  186. package/dist/schemas/generated/insurance.generated.d.ts +10 -0
  187. package/dist/schemas/generated/insurance.generated.d.ts.map +1 -0
  188. package/dist/schemas/generated/killmails.generated.d.ts +55 -0
  189. package/dist/schemas/generated/killmails.generated.d.ts.map +1 -0
  190. package/dist/schemas/generated/location.generated.d.ts +18 -0
  191. package/dist/schemas/generated/location.generated.d.ts.map +1 -0
  192. package/dist/schemas/generated/loyalty.generated.d.ts +18 -0
  193. package/dist/schemas/generated/loyalty.generated.d.ts.map +1 -0
  194. package/dist/schemas/generated/mail.generated.d.ts +68 -0
  195. package/dist/schemas/generated/mail.generated.d.ts.map +1 -0
  196. package/dist/schemas/generated/market.generated.d.ts +201 -0
  197. package/dist/schemas/generated/market.generated.d.ts.map +1 -0
  198. package/dist/schemas/generated/meta.generated.d.ts +26 -0
  199. package/dist/schemas/generated/meta.generated.d.ts.map +1 -0
  200. package/dist/schemas/generated/planetary-interaction.generated.d.ts +90 -0
  201. package/dist/schemas/generated/planetary-interaction.generated.d.ts.map +1 -0
  202. package/dist/schemas/generated/routes.generated.d.ts +5 -0
  203. package/dist/schemas/generated/routes.generated.d.ts.map +1 -0
  204. package/dist/schemas/generated/search.generated.d.ts +15 -0
  205. package/dist/schemas/generated/search.generated.d.ts.map +1 -0
  206. package/dist/schemas/generated/skills.generated.d.ts +32 -0
  207. package/dist/schemas/generated/skills.generated.d.ts.map +1 -0
  208. package/dist/schemas/generated/sovereignty.generated.d.ts +37 -0
  209. package/dist/schemas/generated/sovereignty.generated.d.ts.map +1 -0
  210. package/dist/schemas/generated/status.generated.d.ts +8 -0
  211. package/dist/schemas/generated/status.generated.d.ts.map +1 -0
  212. package/dist/schemas/generated/universe.generated.d.ts +394 -0
  213. package/dist/schemas/generated/universe.generated.d.ts.map +1 -0
  214. package/dist/schemas/generated/wallet.generated.d.ts +411 -0
  215. package/dist/schemas/generated/wallet.generated.d.ts.map +1 -0
  216. package/dist/schemas/generated/wars.generated.d.ts +31 -0
  217. package/dist/schemas/generated/wars.generated.d.ts.map +1 -0
  218. package/dist/schemas/mail.d.ts +13 -0
  219. package/dist/schemas/mail.d.ts.map +1 -1
  220. package/dist/schemas/market.d.ts +104 -4
  221. package/dist/schemas/market.d.ts.map +1 -1
  222. package/dist/schemas/pi.d.ts +3 -0
  223. package/dist/schemas/pi.d.ts.map +1 -1
  224. package/dist/schemas/skills.d.ts +10 -0
  225. package/dist/schemas/skills.d.ts.map +1 -1
  226. package/dist/schemas/sovereignty.d.ts +2 -0
  227. package/dist/schemas/sovereignty.d.ts.map +1 -1
  228. package/dist/schemas/universe.d.ts +11 -2
  229. package/dist/schemas/universe.d.ts.map +1 -1
  230. package/dist/schemas/wallet.d.ts +2 -0
  231. package/dist/schemas/wallet.d.ts.map +1 -1
  232. package/dist/testing/TestDataFactory.d.ts +1 -0
  233. package/dist/testing/TestDataFactory.d.ts.map +1 -1
  234. package/dist/types/api-responses.d.ts +1 -0
  235. package/dist/types/api-responses.d.ts.map +1 -1
  236. package/dist/types/branded.d.ts +23 -0
  237. package/dist/types/branded.d.ts.map +1 -0
  238. package/dist/types/character.d.ts +2 -1
  239. package/dist/types/character.d.ts.map +1 -1
  240. package/dist/types/common.d.ts +10 -0
  241. package/dist/types/common.d.ts.map +1 -1
  242. package/dist/types/generated/esi-spec.generated.d.ts +655 -430
  243. package/dist/types/generated/esi-spec.generated.d.ts.map +1 -1
  244. package/dist/types/generated/spec-alignment.check.d.ts +22 -0
  245. package/dist/types/generated/spec-alignment.check.d.ts.map +1 -0
  246. package/dist/types/market.d.ts +8 -1
  247. package/dist/types/market.d.ts.map +1 -1
  248. package/package.json +235 -200
  249. package/dist/EsiClient.js +0 -251
  250. package/dist/EsiClientBuilder.js +0 -217
  251. package/dist/clients/AccessListsClient.js +0 -20
  252. package/dist/clients/AllianceClient.js +0 -70
  253. package/dist/clients/AssetsClient.js +0 -81
  254. package/dist/clients/BaseEsiClient.js +0 -31
  255. package/dist/clients/CalendarClient.js +0 -54
  256. package/dist/clients/CharacterClient.js +0 -149
  257. package/dist/clients/ClonesClient.js +0 -31
  258. package/dist/clients/ContactsClient.js +0 -104
  259. package/dist/clients/ContractsClient.js +0 -111
  260. package/dist/clients/CorporationsClient.js +0 -227
  261. package/dist/clients/DogmaClient.js +0 -55
  262. package/dist/clients/FactionClient.js +0 -79
  263. package/dist/clients/FittingsClient.js +0 -42
  264. package/dist/clients/FleetClient.js +0 -162
  265. package/dist/clients/FreelanceJobsClient.js +0 -76
  266. package/dist/clients/IncursionsClient.js +0 -19
  267. package/dist/clients/IndustryClient.js +0 -88
  268. package/dist/clients/InsuranceClient.js +0 -19
  269. package/dist/clients/KillmailsClient.js +0 -47
  270. package/dist/clients/LocationClient.js +0 -41
  271. package/dist/clients/LoyaltyClient.js +0 -30
  272. package/dist/clients/MailClient.js +0 -108
  273. package/dist/clients/MarketClient.js +0 -131
  274. package/dist/clients/MercenaryClient.js +0 -27
  275. package/dist/clients/MetaClient.js +0 -56
  276. package/dist/clients/PiClient.js +0 -51
  277. package/dist/clients/RouteClient.js +0 -24
  278. package/dist/clients/SearchClient.js +0 -22
  279. package/dist/clients/SkillsClient.js +0 -41
  280. package/dist/clients/SkyhooksClient.js +0 -35
  281. package/dist/clients/SovereigntyClient.js +0 -27
  282. package/dist/clients/StatusClient.js +0 -19
  283. package/dist/clients/UiClient.js +0 -58
  284. package/dist/clients/UniverseClient.js +0 -277
  285. package/dist/clients/WalletClient.js +0 -82
  286. package/dist/clients/WarsClient.js +0 -37
  287. package/dist/config/configManager.js +0 -47
  288. package/dist/config/jest/globalSetup.js +0 -7
  289. package/dist/config/jest/globalTeardown.js +0 -7
  290. package/dist/config/jest/jest.setup.js +0 -29
  291. package/dist/core/ApiClient.js +0 -123
  292. package/dist/core/ApiClientBuilder.js +0 -54
  293. package/dist/core/ApiRequestHandler.js +0 -444
  294. package/dist/core/BatchRequestHandler.js +0 -66
  295. package/dist/core/ClientRegistry.js +0 -116
  296. package/dist/core/EsiDiagnostics.js +0 -47
  297. package/dist/core/RequestDeduplicator.js +0 -26
  298. package/dist/core/cache/ETagCacheManager.js +0 -184
  299. package/dist/core/cache/ICache.js +0 -2
  300. package/dist/core/circuitBreaker/CircuitBreaker.js +0 -156
  301. package/dist/core/constants.js +0 -7
  302. package/dist/core/endpoints/EndpointDefinition.js +0 -2
  303. package/dist/core/endpoints/accessListEndpoints.js +0 -13
  304. package/dist/core/endpoints/allianceEndpoints.js +0 -31
  305. package/dist/core/endpoints/assetEndpoints.js +0 -50
  306. package/dist/core/endpoints/buildEndpointPath.js +0 -44
  307. package/dist/core/endpoints/calendarEndpoints.js +0 -35
  308. package/dist/core/endpoints/characterEndpoints.js +0 -103
  309. package/dist/core/endpoints/cloneEndpoints.js +0 -19
  310. package/dist/core/endpoints/contactEndpoints.js +0 -72
  311. package/dist/core/endpoints/contractEndpoints.js +0 -70
  312. package/dist/core/endpoints/corporationEndpoints.js +0 -154
  313. package/dist/core/endpoints/createClient.js +0 -156
  314. package/dist/core/endpoints/dogmaEndpoints.js +0 -37
  315. package/dist/core/endpoints/esi-cache-ttls.generated.js +0 -126
  316. package/dist/core/endpoints/esi-rate-limit-groups.generated.js +0 -153
  317. package/dist/core/endpoints/esi-scopes.generated.js +0 -127
  318. package/dist/core/endpoints/factionEndpoints.js +0 -57
  319. package/dist/core/endpoints/fittingEndpoints.js +0 -27
  320. package/dist/core/endpoints/fleetEndpoints.js +0 -101
  321. package/dist/core/endpoints/freelanceJobsEndpoints.js +0 -51
  322. package/dist/core/endpoints/incursionEndpoints.js +0 -13
  323. package/dist/core/endpoints/industryEndpoints.js +0 -61
  324. package/dist/core/endpoints/insuranceEndpoints.js +0 -13
  325. package/dist/core/endpoints/killmailEndpoints.js +0 -28
  326. package/dist/core/endpoints/locationEndpoints.js +0 -27
  327. package/dist/core/endpoints/loyaltyEndpoints.js +0 -21
  328. package/dist/core/endpoints/mailEndpoints.js +0 -66
  329. package/dist/core/endpoints/marketEndpoints.js +0 -80
  330. package/dist/core/endpoints/mercenaryEndpoints.js +0 -19
  331. package/dist/core/endpoints/metaEndpoints.js +0 -10
  332. package/dist/core/endpoints/piEndpoints.js +0 -34
  333. package/dist/core/endpoints/routeEndpoints.js +0 -12
  334. package/dist/core/endpoints/searchEndpoints.js +0 -12
  335. package/dist/core/endpoints/skillEndpoints.js +0 -28
  336. package/dist/core/endpoints/skyhookEndpoints.js +0 -25
  337. package/dist/core/endpoints/sovereigntyEndpoints.js +0 -19
  338. package/dist/core/endpoints/statusEndpoints.js +0 -12
  339. package/dist/core/endpoints/uiEndpoints.js +0 -39
  340. package/dist/core/endpoints/universeEndpoints.js +0 -202
  341. package/dist/core/endpoints/walletEndpoints.js +0 -47
  342. package/dist/core/endpoints/warEndpoints.js +0 -27
  343. package/dist/core/logger/ILogger.js +0 -2
  344. package/dist/core/logger/logger.js +0 -16
  345. package/dist/core/logger/loggerUtil.js +0 -30
  346. package/dist/core/middleware/Middleware.js +0 -42
  347. package/dist/core/pagination/AsyncPaginationIterator.js +0 -27
  348. package/dist/core/pagination/CursorPaginationHandler.js +0 -148
  349. package/dist/core/pagination/PaginationHandler.js +0 -115
  350. package/dist/core/rateLimiter/IRateLimiter.js +0 -2
  351. package/dist/core/rateLimiter/RateLimiter.js +0 -307
  352. package/dist/core/util/error.js +0 -122
  353. package/dist/core/util/headersUtil.js +0 -37
  354. package/dist/core/util/retry.js +0 -9
  355. package/dist/core/util/sleep.js +0 -6
  356. package/dist/core/util/stringUtil.js +0 -6
  357. package/dist/core/util/testHelpers.js +0 -9
  358. package/dist/core/util/validation.js +0 -52
  359. package/dist/schemas/access-lists.js +0 -14
  360. package/dist/schemas/alliance.js +0 -28
  361. package/dist/schemas/assets.js +0 -26
  362. package/dist/schemas/calendar.js +0 -43
  363. package/dist/schemas/character.js +0 -114
  364. package/dist/schemas/clones.js +0 -21
  365. package/dist/schemas/common.js +0 -34
  366. package/dist/schemas/contacts.js +0 -15
  367. package/dist/schemas/contracts.js +0 -58
  368. package/dist/schemas/corporation.js +0 -178
  369. package/dist/schemas/dogma.js +0 -42
  370. package/dist/schemas/faction-warfare.js +0 -92
  371. package/dist/schemas/fittings.js +0 -15
  372. package/dist/schemas/fleet.js +0 -48
  373. package/dist/schemas/freelance-jobs.js +0 -99
  374. package/dist/schemas/incursions.js +0 -14
  375. package/dist/schemas/index.js +0 -48
  376. package/dist/schemas/industry.js +0 -75
  377. package/dist/schemas/insurance.js +0 -12
  378. package/dist/schemas/killmails.js +0 -42
  379. package/dist/schemas/location.js +0 -20
  380. package/dist/schemas/loyalty.js +0 -20
  381. package/dist/schemas/mail.js +0 -30
  382. package/dist/schemas/market.js +0 -26
  383. package/dist/schemas/mercenary.js +0 -22
  384. package/dist/schemas/pi.js +0 -85
  385. package/dist/schemas/skills.js +0 -20
  386. package/dist/schemas/skyhooks.js +0 -30
  387. package/dist/schemas/sovereignty.js +0 -70
  388. package/dist/schemas/status.js +0 -10
  389. package/dist/schemas/universe.js +0 -245
  390. package/dist/schemas/wallet.js +0 -29
  391. package/dist/schemas/wars.js +0 -31
  392. package/dist/testing/TestDataFactory.js +0 -820
  393. package/dist/types/access-lists.js +0 -2
  394. package/dist/types/alliance.js +0 -2
  395. package/dist/types/api-responses.js +0 -72
  396. package/dist/types/assets.js +0 -2
  397. package/dist/types/calendar.js +0 -2
  398. package/dist/types/character.js +0 -2
  399. package/dist/types/clones.js +0 -2
  400. package/dist/types/common.js +0 -2
  401. package/dist/types/contacts.js +0 -2
  402. package/dist/types/contracts.js +0 -2
  403. package/dist/types/corporation.js +0 -2
  404. package/dist/types/dogma.js +0 -2
  405. package/dist/types/faction-warfare.js +0 -2
  406. package/dist/types/fittings.js +0 -2
  407. package/dist/types/fleet.js +0 -2
  408. package/dist/types/freelance-jobs.js +0 -2
  409. package/dist/types/generated/esi-spec.generated.js +0 -6
  410. package/dist/types/incursions.js +0 -2
  411. package/dist/types/industry.js +0 -2
  412. package/dist/types/insurance.js +0 -2
  413. package/dist/types/killmails.js +0 -2
  414. package/dist/types/location.js +0 -2
  415. package/dist/types/loyalty.js +0 -2
  416. package/dist/types/mail.js +0 -2
  417. package/dist/types/market.js +0 -2
  418. package/dist/types/mercenary.js +0 -2
  419. package/dist/types/pi.js +0 -2
  420. package/dist/types/skills.js +0 -2
  421. package/dist/types/skyhooks.js +0 -2
  422. package/dist/types/sovereignty.js +0 -2
  423. package/dist/types/status.js +0 -2
  424. package/dist/types/universe.js +0 -2
  425. package/dist/types/wallet.js +0 -2
  426. package/dist/types/wars.js +0 -2
package/README.md CHANGED
@@ -1,896 +1,967 @@
1
- # ESI.ts
2
-
3
- [![npm version](https://badge.fury.io/js/%40lgriffin%2Fesi.ts.svg)](https://badge.fury.io/js/%40lgriffin%2Fesi.ts)
4
- [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
5
- [![TypeScript](https://img.shields.io/badge/TypeScript-6.0%2B-blue)](https://www.typescriptlang.org/)
6
- [![CI/CD Pipeline](https://github.com/lgriffin/ESI.ts/actions/workflows/ci.yml/badge.svg)](https://github.com/lgriffin/ESI.ts/actions/workflows/ci.yml)
7
- [![PR Validation](https://github.com/lgriffin/ESI.ts/actions/workflows/pr-validation.yml/badge.svg)](https://github.com/lgriffin/ESI.ts/actions/workflows/pr-validation.yml)
8
- [![Coverage](https://img.shields.io/badge/coverage-65%25%2B-brightgreen)](https://github.com/lgriffin/ESI.ts)
9
- [![npm downloads](https://img.shields.io/npm/dm/%40lgriffin/esi.ts)](https://www.npmjs.com/package/@lgriffin/esi.ts)
10
-
11
- A production-grade TypeScript client for the [EVE Online ESI API](https://esi.evetech.net/), with runtime validation, intelligent caching, and full endpoint coverage.
12
-
13
- **All 210 ESI endpoints are defined, tested, and validated against live Tranquility.**
14
-
15
- ## Why ESI.ts vs. OpenAPI-Generated Clients?
16
-
17
- Tools like `openapi-typescript` or `openapi-generator` can produce a typed client from the ESI swagger spec in minutes. They're a reasonable starting point — but they stop at type generation. ESI.ts is a purpose-built SDK that handles the problems you hit _after_ the types compile.
18
-
19
- ### What generators give you
20
-
21
- - TypeScript interfaces from the swagger spec
22
- - Basic request/response typing
23
- - A thin HTTP wrapper
24
-
25
- ### What ESI.ts gives you on top of that
26
-
27
- | Capability | openapi-typescript | ESI.ts |
28
- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
29
- | **Runtime response validation** | None — types are erased at compile time. If CCP changes a field, you get silent data corruption. | Every response is validated at runtime via [Zod](https://zod.dev/) schemas (146 of 210 endpoints). Schema mismatches throw `EsiValidationError` immediately. |
30
- | **Intelligent caching** | None — you build your own. | Three-tier: spec-aware TTL (zero HTTP calls within ESI's `x-cached-seconds` window), ETag conditional GETs, stale-on-error fallback on 5xx. Write operations auto-invalidate related GET caches. |
31
- | **Rate limiting** | None — you build your own. | 36 per-group token buckets extracted from the ESI spec at build time. Market requests can't starve wallet requests. Optional per-user bucketing for multi-character apps. |
32
- | **Pagination** | Manual — you write the page loop. | Automatic offset pagination, cursor-based pagination (Equinox-era endpoints), and streaming `AsyncGenerator` pagination for memory-efficient processing of large datasets. |
33
- | **Retry & resilience** | None. | Exponential backoff with jitter, circuit breaker (closed/open/half-open), automatic 401 token refresh with concurrent coalescing. |
34
- | **Wire format correctness** | Generates from spec, but ESI's spec has inconsistencies (query params documented as body, missing required fields). | Every endpoint tested against live ESI. Wire format bugs (query params vs. body, field naming) are caught and fixed — see the contacts and UI endpoint fixes in v6.1.0. |
35
- | **Batch operations** | None. | `batch()` with bounded concurrency for GET fan-out, `batchPost()` with auto-chunking for large POST payloads. |
36
- | **Domain knowledge** | None — generic HTTP client. | 35 domain clients with typed methods, JSDoc documentation, and input validation (e.g., fleet wing/squad names are capped at 10 characters before hitting the API). |
37
- | **Testing** | Whatever you write. | 121 test suites, 3,224 tests across 7 tiers (TDD unit, BDD behavioral, mocked integration, live smoke, client integration, ESI spec contract, gated auth). 43 runnable example scripts with live output. |
38
-
39
- ### The real problem with generated clients
40
-
41
- The ESI swagger spec is not a perfect source of truth. During live endpoint validation, we discovered:
42
-
43
- - `addContacts`, `editContacts`, and 4 UI endpoints document parameters as request body when ESI actually expects query parameters
44
- - `deleteCharacterContacts` expects comma-separated contact IDs as a query param, not a JSON body
45
- - Fleet wing/squad names have a 10-character limit not documented in the spec
46
- - The `updateMailMetadata` endpoint uses the field name `read`, not `is_read`
47
-
48
- A generated client faithfully reproduces these spec bugs. ESI.ts fixes them.
49
-
50
- ## Installation
51
-
52
- ```bash
53
- npm install @lgriffin/esi.ts
54
- ```
55
-
56
- ### Building from Source
57
-
58
- ```bash
59
- git clone https://github.com/lgriffin/ESI.ts.git
60
- cd ESI.ts
61
- npm install # installs dependencies and compiles (via the prepare script)
62
- ```
63
-
64
- If you've already installed and just need to recompile:
65
-
66
- ```bash
67
- npm run build
68
- ```
69
-
70
- Verify everything works:
71
-
72
- ```bash
73
- npm run example:status # quick smoke test — checks ESI is reachable
74
- npm test # run the full test suite (121 suites, 3,224 tests)
75
- ```
76
-
77
- ## Quick Start
78
-
79
- ```typescript
80
- import { EsiClient } from '@lgriffin/esi.ts';
81
-
82
- const client = new EsiClient();
83
-
84
- // Public data — no auth required
85
- const alliances = await client.alliance.getAlliances();
86
- const character = await client.characters.getCharacterPublicInfo(1689391488);
87
- const system = await client.universe.getSystemById(30000142);
88
- const prices = await client.market.getMarketPrices();
89
-
90
- // Authenticated data — token read from ESI_ACCESS_TOKEN env var
91
- const authedClient = new EsiClient();
92
- const assets = await authedClient.assets.getCharacterAssets(characterId);
93
- const wallet = await authedClient.wallet.getCharacterWallet(characterId);
94
-
95
- // Clean up when done
96
- await client.shutdown();
97
- ```
98
-
99
- ## Configuration
100
-
101
- ```typescript
102
- const client = new EsiClient({
103
- clientId: 'my-app', // User-Agent identifier (default: 'esi-client')
104
- accessToken: 'your-token', // EVE SSO token for authenticated endpoints
105
- baseUrl: 'https://esi.evetech.net', // ESI base URL (default)
106
- onTokenRefresh: async () => newToken, // Auto-refresh on 401 (optional)
107
- language: 'en', // Accept-Language header: en, de, fr, ja, ru, zh, ko, es (default: none)
108
- timeout: 30000, // Request timeout in ms (default: 30000)
109
- retryConfig: {
110
- maxRetries: 3, // Max retry attempts for transient errors (default: 0)
111
- baseDelayMs: 1000, // Initial backoff delay (default: 1000)
112
- maxDelayMs: 30000, // Maximum backoff delay (default: 30000)
113
- retryMutations: false, // Retry POST/PUT/DELETE (default: false, GET only)
114
- },
115
- enableETagCache: true, // ETag caching (default: true)
116
- etagCacheConfig: {
117
- maxEntries: 1000, // Max cached responses (default: 1000)
118
- defaultTtl: 300000, // Fallback TTL in ms (default: 5 min)
119
- cleanupInterval: 60000, // Expired entry cleanup interval (default: 1 min)
120
- },
121
- validateResponse: true, // Runtime Zod validation of ESI responses (default: true)
122
- });
123
- ```
124
-
125
- Retry is disabled by default (`maxRetries: 0`). When enabled, transient errors (502, 503, 504, timeout, rate limit) are retried with exponential backoff and jitter. The circuit breaker is respected — requests are not retried when the circuit is open.
126
-
127
- The access token can be updated at runtime:
128
-
129
- ```typescript
130
- client.setAccessToken('new-token');
131
- ```
132
-
133
- ## Authentication
134
-
135
- Many ESI endpoints require an EVE SSO access token. There are three ways to provide one:
136
-
137
- ### 1. Environment variable (recommended)
138
-
139
- Set `ESI_ACCESS_TOKEN` in your environment or a `.env` file. The client reads it automatically — no token in source code.
140
-
141
- ```bash
142
- # Copy the example and fill in your token
143
- cp .env.example .env
144
- ```
145
-
146
- ```env
147
- ESI_ACCESS_TOKEN=your-eve-sso-access-token
148
- ESI_CLIENT_ID=my-app-name
149
- ```
150
-
151
- If you use a `.env` loader like [dotenv](https://www.npmjs.com/package/dotenv), load it before creating the client:
152
-
153
- ```typescript
154
- import 'dotenv/config';
155
- import { EsiClient } from '@lgriffin/esi.ts';
156
-
157
- const client = new EsiClient();
158
- // Token is picked up from process.env.ESI_ACCESS_TOKEN
159
- ```
160
-
161
- ### 2. Constructor parameter
162
-
163
- Pass the token directly (useful for apps that manage tokens themselves):
164
-
165
- ```typescript
166
- const client = new EsiClient({ accessToken: token });
167
- ```
168
-
169
- ### 3. Runtime update
170
-
171
- Set or refresh the token after construction:
172
-
173
- ```typescript
174
- client.setAccessToken(newToken);
175
- ```
176
-
177
- ### Getting an EVE SSO token
178
-
179
- 1. Register an application at [EVE Developers](https://developers.eveonline.com/)
180
- 2. Set a callback URL and select the ESI scopes your app needs
181
- 3. Implement the [OAuth2 flow](https://docs.esi.evetech.net/docs/sso/) to obtain an access token
182
- 4. Access tokens expire — use the refresh token to get new ones
183
-
184
- ### Automatic Token Refresh
185
-
186
- EVE SSO access tokens expire after 20 minutes. Instead of manually tracking expiry, you can provide a refresh callback — the client will automatically call it on 401, update the token, and retry the request:
187
-
188
- ```typescript
189
- const client = new EsiClient({
190
- accessToken: initialToken,
191
- onTokenRefresh: async () => {
192
- const response = await fetch('https://login.eveonline.com/v2/oauth/token', {
193
- method: 'POST',
194
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
195
- body: new URLSearchParams({
196
- grant_type: 'refresh_token',
197
- refresh_token: myRefreshToken,
198
- client_id: myClientId,
199
- }),
200
- });
201
- const { access_token } = await response.json();
202
- return access_token;
203
- },
204
- });
205
-
206
- // Requests now auto-refresh on 401 — no manual token management needed
207
- const location = await client.location.getCharacterLocation(characterId);
208
- ```
209
-
210
- The token provider can also be set or changed at runtime:
211
-
212
- ```typescript
213
- client.setTokenProvider(myRefreshFunction);
214
- client.setTokenProvider(undefined); // disable auto-refresh
215
- ```
216
-
217
- Key behaviors:
218
-
219
- - Only retries **once** per request — if the refreshed token also gets a 401, the error is thrown
220
- - **Concurrent coalescing** — if multiple requests hit 401 simultaneously, only one refresh call is made
221
- - If the refresh callback throws (e.g., refresh token revoked), a `TOKEN_REFRESH_FAILED` error is raised
222
- - Without a token provider, 401 errors throw immediately as before
223
-
224
- ### Environment variables reference
225
-
226
- | Variable | Description | Default |
227
- | ------------------ | -------------------------------------------- | ------------------------- |
228
- | `ESI_ACCESS_TOKEN` | EVE SSO access token | none |
229
- | `ESI_CLIENT_ID` | User-Agent identifier | `esi-client` |
230
- | `ESI_BASE_URL` | ESI API base URL | `https://esi.evetech.net` |
231
- | `ESI_LOG_LEVEL` | Log level (`error`, `warn`, `info`, `debug`) | `warn` |
232
-
233
- ## Available APIs
234
-
235
- All clients are accessed as properties on the `EsiClient` instance. Authenticated endpoints require an access token.
236
-
237
- | Client | Property | Auth | Examples |
238
- | -------------- | ---------------------- | ---- | -------------------------------------------------------------------------------- |
239
- | Alliance | `client.alliance` | Some | `getAlliances()`, `getAllianceById(id)` |
240
- | Assets | `client.assets` | Yes | `getCharacterAssets(id)` |
241
- | Calendar | `client.calendar` | Yes | `getCalendarEvents(id)` |
242
- | Characters | `client.characters` | Some | `getCharacterPublicInfo(id)`, `getCharacterPortrait(id)` |
243
- | Clones | `client.clones` | Yes | `getCharacterClones(id)` |
244
- | Contacts | `client.contacts` | Yes | `getCharacterContacts(id)`, `postCharacterContacts(id, standing, contactIds)` |
245
- | Contracts | `client.contracts` | Yes | `getCharacterContracts(id)` |
246
- | Corporations | `client.corporations` | Some | `getCorporationInfo(id)`, `getCorporationMembers(id)` |
247
- | Dogma | `client.dogma` | No | `getDogmaAttributes()`, `getDynamicItemInfo(typeId, itemId)` |
248
- | Factions | `client.factions` | Some | `getFactionWarStats()` |
249
- | Fittings | `client.fittings` | Yes | `getFittings(id)`, `createFitting(id, body)` |
250
- | Fleets | `client.fleets` | Yes | `getFleetInformation(id)`, `getFleetMembers(id)` |
251
- | Incursions | `client.incursions` | No | `getIncursions()` |
252
- | Industry | `client.industry` | Some | `getCharacterIndustryJobs(id)` |
253
- | Insurance | `client.insurance` | No | `getInsurancePrices()` |
254
- | Killmails | `client.killmails` | Some | `getKillmail(id, hash)` |
255
- | Location | `client.location` | Yes | `getCharacterLocation(id)` |
256
- | Loyalty | `client.loyalty` | Yes | `getCharacterLoyaltyPoints(id)` |
257
- | Mail | `client.mail` | Yes | `getCharacterMail(id)`, `sendMail(id, body)` |
258
- | Market | `client.market` | Some | `getMarketPrices()`, `getMarketOrders(regionId)` |
259
- | PI | `client.pi` | Yes | `getCharacterPlanets(id)` |
260
- | Route | `client.route` | No | `getRoute(origin, destination)` |
261
- | Search | `client.search` | Some | `search(characterId, query)` |
262
- | Skills | `client.skills` | Yes | `getCharacterSkills(id)` |
263
- | Sovereignty | `client.sovereignty` | No | `getSovereigntySystems()`, `getSovereigntyMap()` |
264
- | Skyhooks | `client.skyhooks` | No | `getSovereigntyHubs()`, `getRaidableSkyhooks()` |
265
- | Mercenary | `client.mercenary` | No | `getMercenaryDens()`, `getMercenaryTacticalOperations()` |
266
- | Access Lists | `client.accessLists` | Yes | `getAccessList(id)` |
267
- | Status | `client.status` | No | `getStatus()` |
268
- | UI | `client.ui` | Yes | `setAutopilotWaypoint(destId, addToBeginning, clear)`, `openNewMailWindow(body)` |
269
- | Universe | `client.universe` | Some | `getSystemById(id)`, `getTypeById(id)` |
270
- | Wallet | `client.wallet` | Yes | `getCharacterWallet(id)` |
271
- | Wars | `client.wars` | No | `getWars()`, `getWarById(id)` |
272
- | Freelance Jobs | `client.freelanceJobs` | Some | `getFreelanceJobs()`, `getFreelanceJobById(id)` |
273
- | Meta | `client.meta` | No | `getOpenApiJson()`, `getOpenApiYaml()` |
274
-
275
- ## Runtime Response Validation
276
-
277
- ESI.ts validates every API response at runtime using [Zod](https://zod.dev/) schemas. 146 of 210 endpoints have schemas attached. If CCP changes the ESI API and the response no longer matches the expected shape, you get an immediate `EsiValidationError` instead of silent data corruption.
278
-
279
- Validation is **on by default**. Extra fields from ESI are preserved via `z.looseObject()` passthrough mode, so new fields added by CCP won't break your application — they flow through to your code untouched.
280
-
281
- ```typescript
282
- import {
283
- EsiClient,
284
- EsiValidationError,
285
- isValidationError,
286
- schemas,
287
- } from '@lgriffin/esi.ts';
288
-
289
- const client = new EsiClient();
290
-
291
- // Validation happens automatically on every request
292
- const character = await client.characters.getCharacterPublicInfo(12345);
293
-
294
- // Disable validation globally if needed
295
- const rawClient = new EsiClient({ validateResponse: false });
296
-
297
- // Use schemas directly for your own validation
298
- const result = schemas.CharacterInfoSchema.safeParse(someData);
299
- if (result.success) {
300
- console.log(result.data.name);
301
- }
302
- ```
303
-
304
- See [guides/RUNTIME-VALIDATION.md](guides/RUNTIME-VALIDATION.md) for the full guide on schemas, error handling, and extending schemas.
305
-
306
- ## Caching
307
-
308
- ETag caching is enabled by default. The client automatically:
309
-
310
- 1. Stores ETag and response data on GET requests
311
- 2. Sends `If-None-Match` on subsequent requests
312
- 3. Returns cached data on `304 Not Modified`
313
- 4. Parses `Cache-Control: max-age` from ESI for per-endpoint TTL
314
- 5. Serves stale cached data when ESI returns 5xx errors
315
- 6. Invalidates related GET caches when POST/PUT/DELETE requests are made
316
-
317
- ```typescript
318
- // Cache stats
319
- const stats = client.getCacheStats();
320
- console.log(`${stats.totalEntries}/${stats.maxEntries} entries cached`);
321
-
322
- // Manual cache operations
323
- client.clearCache();
324
- client.updateCacheConfig({ maxEntries: 2000 });
325
-
326
- // Disable caching entirely
327
- const uncachedClient = new EsiClient({ enableETagCache: false });
328
- ```
329
-
330
- ### Spec-Aware Cache TTLs
331
-
332
- The library reads `x-cached-seconds` from the ESI swagger spec (119 of 195 endpoints). Within the TTL window, repeated GET requests return cached data with **zero HTTP calls** — not even a conditional GET.
333
-
334
- This layers on top of ETag caching in three tiers:
335
-
336
- 1. **Spec TTL** — data can't have changed yet, return cached data immediately
337
- 2. **ETag conditional GET** — data might have changed, send `If-None-Match` to check
338
- 3. **Full request** — no cache entry, fetch fresh data
339
-
340
- ```typescript
341
- const client = new EsiClient();
342
-
343
- // First call — fetches from ESI
344
- const alliances = await client.alliance.getAlliances();
345
-
346
- // Second call within the next 3600s — returns cached data, zero HTTP calls
347
- const same = await client.alliance.getAlliances();
348
- ```
349
-
350
- ## Batch Requests
351
-
352
- Fetch data for multiple IDs with bounded concurrency using `batch()`, or chunk large POST payloads with `batchPost()`:
353
-
354
- ```typescript
355
- import { EsiClient } from '@lgriffin/esi.ts';
356
-
357
- const client = new EsiClient();
358
-
359
- // Fetch 500 type details with at most 10 concurrent requests
360
- const result = await client.batch(
361
- typeIds,
362
- (id) => client.universe.getTypeById(id),
363
- {
364
- concurrency: 10,
365
- onProgress: (done, total) => console.log(`${done}/${total}`),
366
- },
367
- );
368
-
369
- // result.results: Map<number, T> — successful responses
370
- // result.errors: Map<number, Error> — failed requests
371
- console.log(`${result.results.size} succeeded, ${result.errors.size} failed`);
372
- ```
373
-
374
- For POST endpoints that accept arrays (e.g., `postUniverseNames` with a 1000-ID limit), `batchPost` auto-chunks and concatenates:
375
-
376
- ```typescript
377
- const allNames = await client.batchPost(
378
- largeIdArray,
379
- (chunk) => client.universe.postUniverseNames(chunk),
380
- 1000, // chunk size
381
- );
382
- ```
383
-
384
- ## Streaming Pagination
385
-
386
- For large paginated endpoints (market orders, contracts, assets), streaming yields one page at a time via `AsyncGenerator` instead of eagerly fetching all pages into memory:
387
-
388
- ```typescript
389
- import { EsiClient } from '@lgriffin/esi.ts';
390
-
391
- const client = new EsiClient();
392
-
393
- // Stream all market orders in The Forge, page by page
394
- for await (const page of client.market.streamMarketOrders(10000002)) {
395
- console.log(
396
- `Page ${page.page}/${page.totalPages}: ${page.data.length} orders`,
397
- );
398
-
399
- // Process each order as it arrives
400
- for (const order of page.data) {
401
- if (order.is_buy_order && order.price > 1_000_000) {
402
- console.log(`High-value buy: ${order.type_id} @ ${order.price} ISK`);
403
- }
404
- }
405
-
406
- // Early termination — stops fetching remaining pages
407
- if (page.page >= 3) break;
408
- }
409
- ```
410
-
411
- Available streaming methods:
412
-
413
- - **MarketClient** — `streamMarketOrders`, `streamMarketTypes`, `streamCharacterOrderHistory`, `streamCorporationOrders`, `streamCorporationOrderHistory`, `streamMarketOrdersInStructure`
414
- - **ContractsClient** — `streamPublicContracts`, `streamCharacterContracts`, `streamCorporationContracts`
415
- - **WalletClient** — `streamCharacterWalletJournal`, `streamCorporationWalletJournal`, `streamCharacterWalletTransactions`
416
- - **AssetsClient** — `streamCharacterAssets`, `streamCorporationAssets`
417
- - **KillmailsClient** — `streamCharacterRecentKillmails`, `streamCorporationRecentKillmails`
418
-
419
- Try it: `npm run example:streaming`
420
-
421
- ## Cursor-based Pagination
422
-
423
- Newer ESI routes (Freelance Jobs, and future routes) use cursor-based pagination with opaque `before`/`after` tokens in the response body. See the [ESI blog post](https://developers.eveonline.com/blog/changing-pagination-turning-a-new-page) for background.
424
-
425
- ```typescript
426
- import { EsiClient, fetchAllCursorPages } from '@lgriffin/esi.ts';
427
-
428
- const client = new EsiClient();
429
-
430
- // Fetch first page — returns { cursor: { before, after }, freelance_jobs: [...] }
431
- const page = await client.freelanceJobs.getFreelanceJobs();
432
- console.log(page.freelance_jobs); // job records
433
- console.log(page.cursor.after); // opaque token for next page
434
-
435
- // Fetch next page using the cursor
436
- const nextPage = await client.freelanceJobs.getFreelanceJobs(
437
- undefined,
438
- page.cursor.after,
439
- );
440
-
441
- // Auto-fetch all pages in one call
442
- const allJobs = await fetchAllCursorPages(
443
- (before, after) => client.freelanceJobs.getFreelanceJobs(before, after),
444
- (response) => response.freelance_jobs,
445
- (response) => response.cursor,
446
- );
447
-
448
- // Authenticated endpoints — character/corporation freelance jobs
449
- const authedClient = new EsiClient({ accessToken: 'your-token' });
450
- const myJobs =
451
- await authedClient.freelanceJobs.getCharacterFreelanceJobs(characterId);
452
- const corpJobs =
453
- await authedClient.freelanceJobs.getCorporationFreelanceJobs(corporationId);
454
- ```
455
-
456
- **Polling for changes** — cursor tokens persist across sessions, so you can save the last `after` token and poll later to get only records that changed:
457
-
458
- ```typescript
459
- // After initial scan, save the final cursor
460
- let savedCursor = lastPage.cursor.after;
461
-
462
- // Later: check for updates (hours, days, or weeks later)
463
- const updates = await client.freelanceJobs.getFreelanceJobs(
464
- undefined,
465
- savedCursor,
466
- );
467
- if (updates.freelance_jobs.length > 0) {
468
- // Process changed records — duplicates are expected for modified records
469
- savedCursor = updates.cursor.after;
470
- }
471
- ```
472
-
473
- Key points:
474
-
475
- - Cursor tokens are **opaque strings** — never parse or validate them
476
- - An **empty result array** signals the end of the dataset (not a short page)
477
- - **Duplicates across pages** are expected when records are modified between requests
478
- - Existing offset-based routes (`getMarketOrders`, etc.) are unchanged
479
-
480
- ## Generated Types
481
-
482
- The library includes TypeScript interfaces generated directly from the ESI swagger spec, available as the `EsiSpec` namespace. These are guaranteed to match the live spec and complement the hand-written types:
483
-
484
- ```typescript
485
- import { EsiSpec } from '@lgriffin/esi.ts';
486
-
487
- // Generated type — exact spec field names and optionality
488
- const order: EsiSpec.GetMarketsRegionIdOrders200Ok = {
489
- order_id: 123,
490
- type_id: 34,
491
- price: 5.5,
492
- volume_remain: 1000,
493
- volume_total: 5000,
494
- is_buy_order: false,
495
- // ...
496
- };
497
- ```
498
-
499
- To regenerate types from the latest ESI spec:
500
-
501
- ```bash
502
- npm run generate:types # fetches spec, generates 147 interfaces + cache TTL map + rate limit groups + scope map
503
- npm run validate:esi # reports type drift between hand-written and generated types
504
- ```
505
-
506
- ## ESI Scopes
507
-
508
- The library includes a generated scope-to-endpoint mapping extracted from the ESI swagger spec. Use it to check which OAuth scopes an endpoint requires before making a request:
509
-
510
- ```typescript
511
- import { esiEndpointScopes, EsiScope } from '@lgriffin/esi.ts';
512
-
513
- // Look up scopes for a specific endpoint
514
- const walletScopes = esiEndpointScopes['GET:characters/{character_id}/wallet'];
515
- // → ['esi-wallet.read_character_wallet.v1']
516
-
517
- // Check if an endpoint requires auth
518
- const isPublic = !esiEndpointScopes['GET:universe/types/{type_id}'];
519
- // → true (public endpoint, no scopes needed)
520
-
521
- // Type-safe scope values
522
- const scope: EsiScope = 'esi-assets.read_assets.v1';
523
- ```
524
-
525
- ## Error Handling
526
-
527
- API errors throw `EsiError` with `statusCode`, `message`, and `url` properties:
528
-
529
- ```typescript
530
- import {
531
- EsiError,
532
- TimeoutError,
533
- EsiValidationError,
534
- isTimeout,
535
- isRetryable,
536
- isValidationError,
537
- } from '@lgriffin/esi.ts';
538
-
539
- try {
540
- const alliance = await client.alliance.getAllianceById(99999999);
541
- console.log('Alliance:', alliance.name);
542
- } catch (err) {
543
- if (isValidationError(err)) {
544
- console.log('Response validation failed:', err.validationError);
545
- } else if (isTimeout(err)) {
546
- console.log(`Request timed out after ${err.timeoutMs}ms`);
547
- } else if (err instanceof EsiError) {
548
- console.log(`ESI error ${err.statusCode}: ${err.message}`);
549
- console.log(`Retryable: ${err.retryable}`);
550
- }
551
- }
552
- ```
553
-
554
- - **204 No Content** — returns `undefined` (valid for DELETE/POST actions)
555
- - **304 Not Modified** — handled internally, returns cached data
556
- - **4xx/5xx** — throws `EsiError`
557
- - **5xx with cache** — returns stale cached data instead of throwing
558
- - **Timeout** — throws `TimeoutError` (extends `EsiError` with `statusCode: 0` and `timeoutMs`)
559
- - **Retryable errors** — `EsiError.retryable` returns `true` for 502, 503, 504, 420, 429, and timeouts
560
- - **Validation errors** — throws `EsiValidationError` (extends `EsiError`) when response data doesn't match the expected Zod schema
561
-
562
- ## Response Metadata
563
-
564
- Use `withMetadata()` to get response headers, cache status, rate limit info, and timing alongside the data:
565
-
566
- ```typescript
567
- const metaClient = client.alliance.withMetadata();
568
- const result = await metaClient.getAllianceById(99000001);
569
-
570
- console.log(result.data.name); // "Goonswarm Federation"
571
- console.log(result.meta.fromCache); // true if served from cache
572
- console.log(result.meta.cacheHitType); // 'spec-ttl' | 'etag-304' | 'stale-on-error'
573
- console.log(result.meta.responseTimeMs); // milliseconds
574
- console.log(result.meta.rateLimit); // { remaining, limit, used, group }
575
- console.log(result.meta.requestId); // ESI request ID for debugging
576
- ```
577
-
578
- The `meta` object includes:
579
-
580
- | Field | Type | Description |
581
- | ---------------- | ------------------------ | ------------------------------------------------- |
582
- | `headers` | `Record<string, string>` | Raw response headers |
583
- | `fromCache` | `boolean` | Whether data was served from cache |
584
- | `stale` | `boolean` | Whether cached data is stale (5xx fallback) |
585
- | `cacheHitType` | `string?` | `'spec-ttl'`, `'etag-304'`, or `'stale-on-error'` |
586
- | `rateLimit` | `RateLimitMeta?` | Rate limit status from ESI headers |
587
- | `responseTimeMs` | `number?` | Request duration in milliseconds |
588
- | `requestId` | `string?` | ESI request ID |
589
- | `warning` | `object?` | ESI deprecation warning |
590
-
591
- ## Rate Limiting
592
-
593
- ESI.ts automatically manages rate limiting using ESI's per-group token bucket system. The 36 rate limit groups from the ESI OpenAPI spec are extracted at build time, so each group (e.g., `market-order`, `char-notification`) gets its own independent bucket. A burst of market requests won't starve unrelated endpoints.
594
-
595
- Rate limiting works out of the box with no configuration. For multi-character applications, enable per-user bucketing:
596
-
597
- ```typescript
598
- import { EsiClient } from '@lgriffin/esi.ts';
599
-
600
- const client = new EsiClient({
601
- rateLimiterConfig: {
602
- userKeyExtractor: (headers) => headers['authorization'] ?? 'anon',
603
- },
604
- });
605
- ```
606
-
607
- Monitor rate limit status per group:
608
-
609
- ```typescript
610
- const limiter = client.getRateLimiter();
611
-
612
- // Worst-case across all groups (backward-compatible)
613
- const status = limiter.getStatus();
614
- console.log(status.remaining, status.limit, status.group);
615
-
616
- // Specific group
617
- const marketStatus = limiter.getGroupStatus('market-order');
618
- console.log(marketStatus?.remaining); // tokens remaining in this group
619
-
620
- // All active groups
621
- const all = limiter.getAllGroupStatuses();
622
- for (const [group, info] of all) {
623
- console.log(`${group}: ${info.remaining}/${info.limit}`);
624
- }
625
-
626
- // Check if a specific group is blocked
627
- console.log(limiter.isBlocked('char-notification')); // true if 429'd
628
- ```
629
-
630
- ## Lightweight Clients
631
-
632
- If you only need a subset of APIs, use `CustomEsiClient` or `EsiClientBuilder` to load only what you need:
633
-
634
- ```typescript
635
- import { EsiClientBuilder } from '@lgriffin/esi.ts';
636
-
637
- const client = new EsiClientBuilder()
638
- .addClients(['market', 'universe', 'characters'])
639
- .withClientId('my-trading-bot')
640
- .withAccessToken('your-token')
641
- .build();
642
-
643
- const prices = await client.market?.getMarketPrices();
644
- const system = await client.universe?.getSystemById(30000142);
645
- ```
646
-
647
- Or create standalone single-API clients:
648
-
649
- ```typescript
650
- import { EsiApiFactory } from '@lgriffin/esi.ts';
651
-
652
- const marketClient = EsiApiFactory.createMarketClient({
653
- clientId: 'price-checker',
654
- });
655
- const prices = await marketClient.getMarketPrices();
656
- ```
657
-
658
- ## Endpoint Coverage
659
-
660
- All 210 ESI endpoint definitions have been validated against live Tranquility. This table summarizes the validation approach:
661
-
662
- | Category | Endpoints | Method |
663
- | --------------------------- | --------- | -------------------------------------------------- |
664
- | Public GETs | 78 | 43 runnable example scripts with captured output |
665
- | Authenticated GETs | 72 | Example scripts + live testing with EVE SSO tokens |
666
- | Contacts (POST/PUT/DELETE) | 3 | Live create/edit/delete lifecycle |
667
- | Fittings (POST/DELETE) | 2 | Live create/delete lifecycle |
668
- | Mail (POST/PUT/DELETE) | 5 | Live send/label/metadata/delete lifecycle |
669
- | UI (POST) | 5 | Live testing with EVE client running |
670
- | Calendar (PUT) | 1 | Live RSVP to event |
671
- | Fleet (GET/POST/PUT/DELETE) | 14 | Live fleet with two characters |
672
- | Assets POST | 3 | Live asset location/name queries |
673
- | CSPA (POST) | 1 | Live charge cost calculation |
674
- | Dogma dynamic (GET) | 1 | Live mutaplasmid item query |
675
- | Universe POST helpers | 3 | Live name resolution and affiliation |
676
- | Freelance Jobs (GET) | 4 | Live queries (graceful 404 for no active jobs) |
677
-
678
- ## Examples
679
-
680
- 43 runnable examples are in the `examples/` directory.
681
-
682
- ### Public Endpoints (no auth needed)
683
-
684
- ```bash
685
- npm run example:status # Server status — quickest smoke test
686
- npm run example:character # Character public info, portrait, corporation
687
- npm run example:universe # Solar system, constellation, region, station
688
- npm run example:market # Average prices + Tritanium price history
689
- npm run example:alliance # Alliance info + member corporations
690
- npm run example:route # Jita-to-Amarr route with system names
691
- npm run example:wars # Recent wars with aggressor/defender details
692
- npm run example:sovereignty # Nullsec sovereignty map + active campaigns
693
- npm run example:industry # Industry facilities, cost indices, insurance
694
- npm run example:incursions # Active incursions + faction warfare stats
695
- npm run example:dogma # Item type details + dogma attributes
696
- npm run example:contracts # Public region contracts + auction bids/items
697
- npm run example:rate-limiting # Rate limiter & pagination demonstration
698
- npm run example:cursor-pagination # Freelance Jobs with cursor pagination
699
- npm run example:streaming # Streaming pagination for large datasets
700
- npm run example:token-refresh # Automatic token refresh on 401
701
- npm run example:universe-encyclopedia # Ancestries, bloodlines, races, celestials
702
- npm run example:dogma-meta-sov # Dogma effects, sovereignty, meta endpoint
703
- npm run example:faction-details # Faction warfare leaderboards and stats
704
- ```
705
-
706
- ### Authenticated Endpoints (require ESI_ACCESS_TOKEN)
707
-
708
- ```bash
709
- npm run example # Full character profile assembly
710
- npm run example:wallet # Wallet balance, journal, transactions
711
- npm run example:skills # Trained skills, queue, attributes
712
- npm run example:assets # Asset inventory with bulk name lookup
713
- npm run example:killmails # Recent killmails + full details
714
- npm run example:fleet # Fleet info, members, wing/squad structure
715
- npm run example:mail # Inbox headers, labels, mailing lists
716
- npm run example:location # Current system, online status, ship
717
- npm run example:fittings # Saved fittings + clone state + implants
718
- npm run example:contacts # Contact list with standings + labels
719
- npm run example:character-details # Blueprints, roles, standings, medals
720
- npm run example:corporation-details # Corp members, divisions, structures
721
- npm run example:calendar-search # Calendar events + character search
722
- npm run example:loyalty-pi # Loyalty points + planetary interaction
723
- npm run example:industry-mining # Industry jobs + mining ledger
724
- npm run example:market-orders # Character/corp market orders
725
- npm run example:corp-contracts-wallet # Corp contracts, contacts, wallets
726
- ```
727
-
728
- ### Write Operations (require specific scopes + caution)
729
-
730
- ```bash
731
- npm run example:write-ops # Contacts, fittings, mail, UI lifecycle tests
732
- npm run example:universe-posts # Name resolution + character affiliation (public)
733
- npm run example:freelance-jobs # Freelance job queries
734
- ```
735
-
736
- ### Parallel Requests
737
-
738
- ```typescript
739
- const [character, portrait, corp] = await Promise.all([
740
- client.characters.getCharacterPublicInfo(characterId),
741
- client.characters.getCharacterPortrait(characterId),
742
- client.corporations.getCorporationInfo(corporationId),
743
- ]);
744
-
745
- console.log(`${character.name} [${corp.ticker}]`);
746
- ```
747
-
748
- ### Market Analysis
749
-
750
- ```typescript
751
- const [orders, history] = await Promise.all([
752
- client.market.getMarketOrders(regionId),
753
- client.market.getMarketHistory(regionId, typeId),
754
- ]);
755
-
756
- const buyOrders = orders.filter((o) => o.is_buy_order);
757
- const sellOrders = orders.filter((o) => !o.is_buy_order);
758
-
759
- console.log(`Best buy: ${Math.max(...buyOrders.map((o) => o.price))}`);
760
- console.log(`Best sell: ${Math.min(...sellOrders.map((o) => o.price))}`);
761
- ```
762
-
763
- ## Resource Management
764
-
765
- Always call `shutdown()` when you're done to clean up cache timers:
766
-
767
- ```typescript
768
- const client = new EsiClient();
769
- try {
770
- const status = await client.status.getStatus();
771
- console.log(status.server_version);
772
- } finally {
773
- await client.shutdown();
774
- }
775
- ```
776
-
777
- ## Testing
778
-
779
- ESI.ts has a comprehensive multi-tier testing strategy:
780
-
781
- | Tier | Tests | Purpose |
782
- | ---------------------- | ---------------- | ---------------------------------------------------------------- |
783
- | **TDD unit tests** | 81 files | Every client method, endpoint path, query param, and body format |
784
- | **BDD scenario tests** | 40 feature files | Behavioral specifications in Gherkin (Given/When/Then) |
785
- | **Mocked integration** | Full suite | Cross-layer request flow with jest-fetch-mock |
786
- | **Live smoke tests** | 43 examples | Every endpoint against live Tranquility |
787
- | **ESI spec contract** | 10 tests | Endpoint definitions validated against live swagger spec |
788
- | **Gated auth tests** | 33 tests | Authenticated endpoints with real tokens |
789
-
790
- ```bash
791
- npm test # Unit + BDD tests (121 suites, 3,224 tests)
792
- npm run coverage # Tests with coverage report (thresholds enforced)
793
- npm run bdd # BDD scenario tests only
794
- ```
795
-
796
- Coverage thresholds are enforced in CI: branches 50%, functions 50%, lines 65%, statements 65%.
797
-
798
- See [guides/TESTING.md](guides/TESTING.md) for the full testing guide, and [guides/ARCHITECTURE.md](guides/ARCHITECTURE.md) for architecture diagrams.
799
-
800
- ## Development
801
-
802
- ### Prerequisites
803
-
804
- - Node.js 18+
805
- - npm
806
-
807
- ### Code Quality Tools
808
-
809
- The project uses a comprehensive suite of static analysis and code quality tools:
810
-
811
- | Tool | Purpose | Command |
812
- | ------------------------------------------------------------------------------------ | ------------------------------------------------------- | ------------------------------ |
813
- | [ESLint](https://eslint.org/) | Linting with TypeScript, security, and code smell rules | `npm run lint` |
814
- | [Prettier](https://prettier.io/) | Code formatting | `npm run format:check` |
815
- | [knip](https://knip.dev/) | Dead code and unused export detection | `npm run knip` |
816
- | [eslint-plugin-security](https://github.com/eslint-community/eslint-plugin-security) | Security anti-pattern detection | Integrated into `npm run lint` |
817
- | [eslint-plugin-sonarjs](https://github.com/SonarSource/eslint-plugin-sonarjs) | Cognitive complexity and code smell detection | Integrated into `npm run lint` |
818
- | [husky](https://typicode.github.io/husky/) | Git pre-commit hooks | Automatic on commit |
819
- | [lint-staged](https://github.com/lint-staged/lint-staged) | Run linters on staged files only | Automatic on commit |
820
-
821
- ### Available Scripts
822
-
823
- ```bash
824
- # Development
825
- npm run build # Compile TypeScript
826
- npm run lint # Run ESLint
827
- npm run lint:fix # Run ESLint with auto-fix
828
- npm run format # Format code with Prettier
829
- npm run format:check # Check formatting without modifying
830
-
831
- # Testing
832
- npm test # Unit tests (121 suites, 3,224 tests)
833
- npm run test:all # Unit + improved + BDD tests
834
- npm run coverage # Tests with coverage report (thresholds enforced)
835
- npm run bdd # BDD scenario tests
836
-
837
- # Static Analysis
838
- npm run knip # Detect dead code and unused exports
839
- npm run validate:esi # Validate endpoints against live ESI swagger spec
840
- npm run validate # Run all checks: lint, format, build, coverage, knip
841
- npm run generate:types # Regenerate TypeScript interfaces from ESI swagger spec
842
-
843
- # Documentation
844
- npm run docs # Generate TypeDoc API documentation
845
- npm run docs:serve # Serve docs locally on port 8080
846
- ```
847
-
848
- ### ESI Endpoint Validation
849
-
850
- To verify that the codebase endpoint definitions match the live ESI swagger spec:
851
-
852
- ```bash
853
- npm run validate:esi
854
- ```
855
-
856
- This fetches `https://esi.evetech.net/latest/swagger.json` and reports:
857
-
858
- - Endpoints in the codebase that are no longer in the ESI spec
859
- - Endpoints in the ESI spec that the codebase doesn't cover
860
- - HTTP method mismatches between codebase and spec
861
-
862
- ### Pre-commit Hooks
863
-
864
- The project uses husky with lint-staged to run ESLint and Prettier on staged files before each commit. This is set up automatically when you run `npm install`.
865
-
866
- ### CI/CD
867
-
868
- Every pull request runs the full validation suite:
869
-
870
- - ESLint (with security and sonarjs plugins)
871
- - Prettier formatting check
872
- - TypeScript compilation
873
- - Generated types staleness check (regenerates from live ESI spec and verifies no diff)
874
- - Unit tests across Node.js 18, 20, and 22
875
- - BDD scenario tests
876
- - Coverage threshold enforcement (branches: 50%, functions: 50%, lines: 65%, statements: 65%)
877
- - Dead code detection via knip
878
- - npm security audit
879
-
880
- See [.github/workflows/README.md](.github/workflows/README.md) for full workflow details.
881
-
882
- ## Contributing
883
-
884
- 1. Fork the repository
885
- 2. Create a feature branch
886
- 3. Write tests for your changes
887
- 4. Run `npm run validate` to check everything passes
888
- 5. Open a Pull Request
889
-
890
- ## License
891
-
892
- GPL-3.0-or-later - see the [LICENSE](LICENSE) file for details.
893
-
894
- ---
895
-
896
- **o7**
1
+ # ESI.ts
2
+
3
+ [![npm version](https://badge.fury.io/js/%40lgriffin%2Fesi.ts.svg)](https://badge.fury.io/js/%40lgriffin%2Fesi.ts)
4
+ [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
5
+ [![TypeScript](https://img.shields.io/badge/TypeScript-6.0%2B-blue)](https://www.typescriptlang.org/)
6
+ [![CI/CD Pipeline](https://github.com/lgriffin/ESI.ts/actions/workflows/ci.yml/badge.svg)](https://github.com/lgriffin/ESI.ts/actions/workflows/ci.yml)
7
+ [![PR Validation](https://github.com/lgriffin/ESI.ts/actions/workflows/pr-validation.yml/badge.svg)](https://github.com/lgriffin/ESI.ts/actions/workflows/pr-validation.yml)
8
+ [![Coverage](https://img.shields.io/badge/coverage-90%25%2B-brightgreen)](https://github.com/lgriffin/ESI.ts)
9
+ [![npm downloads](https://img.shields.io/npm/dm/%40lgriffin/esi.ts)](https://www.npmjs.com/package/@lgriffin/esi.ts)
10
+
11
+ A production-grade TypeScript client for the [EVE Online ESI API](https://esi.evetech.net/), built on the **OpenAPI 3.1 spec**, with runtime validation, intelligent caching, and full endpoint coverage.
12
+
13
+ **v8.0.0** — Architecture overhaul: unified client construction (all three client surfaces now get identical middleware defaults via `configureApiClient()`), decomposed request pipeline (`requestPipeline/` modules), 57 new streaming methods across 16 domain clients, opt-in request body validation, injectable `IRetryStrategy`, configurable circuit breaker keying, and typed `createClient()` return types.
14
+
15
+ **208 endpoint definitions — 194 from the public ESI OpenAPI spec, plus 14 for newer EVE features (Equinox sovereignty, orbital skyhooks, mercenary dens, access lists, freelance jobs). All 206 exercisable endpoints validated against live Tranquility on 2026-07-08.**
16
+
17
+ ## Why ESI.ts vs. OpenAPI-Generated Clients?
18
+
19
+ Tools like `openapi-typescript` or `openapi-generator` can produce a typed client from the ESI OpenAPI spec in minutes. They're a reasonable starting point — but they stop at type generation. ESI.ts is a purpose-built SDK that handles the problems you hit _after_ the types compile.
20
+
21
+ ### What generators give you
22
+
23
+ - TypeScript interfaces from the OpenAPI spec
24
+ - Basic request/response typing
25
+ - A thin HTTP wrapper
26
+
27
+ ### What ESI.ts gives you on top of that
28
+
29
+ | Capability | openapi-typescript | ESI.ts |
30
+ | ------------------------------- | ------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
31
+ | **Runtime response validation** | None — types are erased at compile time. If CCP changes a field, you get silent data corruption. | Every GET response is validated at runtime via [Zod](https://zod.dev/) schemas — all 173 GET endpoints have schemas. Schema mismatches throw `EsiValidationError` immediately. |
32
+ | **Intelligent caching** | None — you build your own. | Three-tier: spec-aware TTL (zero HTTP calls within ESI's `x-cached-seconds` window), ETag conditional GETs, stale-on-error fallback on 5xx. Write operations auto-invalidate related GET caches. |
33
+ | **Rate limiting** | None — you build your own. | 36 per-group token buckets extracted from the ESI spec at build time. Market requests can't starve wallet requests. Optional per-user bucketing for multi-character apps. |
34
+ | **Pagination** | Manual — you write the page loop. | Automatic offset pagination, cursor-based pagination (Equinox-era endpoints), and streaming `AsyncGenerator` pagination for memory-efficient processing of large datasets. |
35
+ | **Retry & resilience** | None. | Exponential backoff with jitter, circuit breaker (closed/open/half-open), automatic 401 token refresh with concurrent coalescing. |
36
+ | **Wire format correctness** | Generates from spec, but ESI's spec has inconsistencies (query params documented as body, missing required fields). | Every endpoint tested against live ESI. Wire format bugs (query params vs. body, field naming) are caught and fixed — see the contacts and UI endpoint fixes in v6.1.0. |
37
+ | **Batch operations** | None. | `batch()` with bounded concurrency for GET fan-out, `batchPost()` with auto-chunking for large POST payloads. |
38
+ | **Domain knowledge** | None — generic HTTP client. | 35 domain clients with typed methods, JSDoc documentation, and input validation (e.g., fleet wing/squad names are capped at 10 characters before hitting the API). |
39
+ | **Streaming pagination** | None. | 21 domain clients with 73+ `stream*` methods via `AsyncGenerator` — process large datasets page-by-page without loading everything into memory. |
40
+ | **Testing** | Whatever you write. | 95+ test suites, 3,800+ tests across 9 tiers including property-based fuzzing (fast-check), mutation testing (Stryker), deep contract tests against live OpenAPI spec, and consumer type tests (tsd). 43 runnable example scripts. |
41
+
42
+ ### The real problem with generated clients
43
+
44
+ The ESI OpenAPI spec is not a perfect source of truth. During live endpoint validation against the OpenAPI 3.1 spec, we discovered:
45
+
46
+ - `addContacts`, `editContacts`, and 4 UI endpoints document parameters as request body when ESI actually expects query parameters
47
+ - `deleteCharacterContacts` expects comma-separated contact IDs as a query param, not a JSON body
48
+ - Fleet wing/squad names have a 10-character limit not documented in the spec
49
+ - The `updateMailMetadata` endpoint uses the field name `read`, not `is_read`
50
+
51
+ A generated client faithfully reproduces these spec bugs. ESI.ts fixes them.
52
+
53
+ ## Installation
54
+
55
+ ```bash
56
+ npm install @lgriffin/esi.ts
57
+ ```
58
+
59
+ ### Building from Source
60
+
61
+ ```bash
62
+ git clone https://github.com/lgriffin/ESI.ts.git
63
+ cd ESI.ts
64
+ npm install # installs dependencies and compiles (via the prepare script)
65
+ ```
66
+
67
+ If you've already installed and just need to recompile:
68
+
69
+ ```bash
70
+ npm run build
71
+ ```
72
+
73
+ Verify everything works:
74
+
75
+ ```bash
76
+ npm run example:status # quick smoke test — checks ESI is reachable
77
+ npm test # run the full test suite (95+ suites, 3,800+ tests)
78
+ ```
79
+
80
+ ## Quick Start
81
+
82
+ ```typescript
83
+ import { EsiClient } from '@lgriffin/esi.ts';
84
+
85
+ const client = new EsiClient();
86
+
87
+ // Public data — no auth required
88
+ const alliances = await client.alliance.getAlliances();
89
+ const character = await client.characters.getCharacterPublicInfo(1689391488);
90
+ const system = await client.universe.getSystemById(30000142);
91
+ const prices = await client.market.getMarketPrices();
92
+
93
+ // Authenticated data — token read from ESI_ACCESS_TOKEN env var
94
+ const authedClient = new EsiClient();
95
+ const assets = await authedClient.assets.getCharacterAssets(characterId);
96
+ const wallet = await authedClient.wallet.getCharacterWallet(characterId);
97
+
98
+ // Clean up when done
99
+ await client.shutdown();
100
+ ```
101
+
102
+ ## Configuration
103
+
104
+ ```typescript
105
+ const client = new EsiClient({
106
+ clientId: 'my-app', // User-Agent identifier (default: 'esi-client')
107
+ accessToken: 'your-token', // EVE SSO token for authenticated endpoints
108
+ baseUrl: 'https://esi.evetech.net', // ESI base URL (default)
109
+ onTokenRefresh: async () => newToken, // Auto-refresh on 401 (optional)
110
+ language: 'en', // Accept-Language header: en, de, fr, ja, ru, zh, ko, es (default: none)
111
+ timeout: 30000, // Request timeout in ms (default: 30000)
112
+ retryConfig: {
113
+ maxRetries: 3, // Max retry attempts for transient errors (default: 0)
114
+ baseDelayMs: 1000, // Initial backoff delay (default: 1000)
115
+ maxDelayMs: 30000, // Maximum backoff delay (default: 30000)
116
+ retryMutations: false, // Retry POST/PUT/DELETE (default: false, GET only)
117
+ },
118
+ enableETagCache: true, // ETag caching (default: true)
119
+ etagCacheConfig: {
120
+ maxEntries: 1000, // Max cached responses (default: 1000)
121
+ defaultTtl: 300000, // Fallback TTL in ms (default: 5 min)
122
+ cleanupInterval: 60000, // Expired entry cleanup interval (default: 1 min)
123
+ },
124
+ validateResponse: true, // Runtime Zod validation of ESI responses (default: true)
125
+ validateRequest: false, // Opt-in request body Zod validation for POST/PUT/DELETE (default: false)
126
+ retryStrategy: customRetryStrategy, // Injectable IRetryStrategy (default: built-in exponential backoff)
127
+ circuitBreakerConfig: {
128
+ keyStrategy: 'resolved', // CB keying: 'resolved' (per-URL) or 'template' (per-route) (default: 'resolved')
129
+ cleanupIntervalMs: 300000, // Automatic stale circuit cleanup interval (default: 5 min)
130
+ },
131
+ });
132
+ ```
133
+
134
+ Retry is disabled by default (`maxRetries: 0`). When enabled, transient errors (502, 503, 504, timeout, rate limit) are retried with exponential backoff and jitter. The circuit breaker is respected — requests are not retried when the circuit is open.
135
+
136
+ The access token can be updated at runtime:
137
+
138
+ ```typescript
139
+ client.setAccessToken('new-token');
140
+ ```
141
+
142
+ ## Authentication
143
+
144
+ Many ESI endpoints require an EVE SSO access token. There are three ways to provide one:
145
+
146
+ ### 1. Environment variable (recommended)
147
+
148
+ Set `ESI_ACCESS_TOKEN` in your environment or a `.env` file. The client reads it automatically — no token in source code.
149
+
150
+ ```bash
151
+ # Copy the example and fill in your token
152
+ cp .env.example .env
153
+ ```
154
+
155
+ ```env
156
+ ESI_ACCESS_TOKEN=your-eve-sso-access-token
157
+ ESI_CLIENT_ID=my-app-name
158
+ ```
159
+
160
+ If you use a `.env` loader like [dotenv](https://www.npmjs.com/package/dotenv), load it before creating the client:
161
+
162
+ ```typescript
163
+ import 'dotenv/config';
164
+ import { EsiClient } from '@lgriffin/esi.ts';
165
+
166
+ const client = new EsiClient();
167
+ // Token is picked up from process.env.ESI_ACCESS_TOKEN
168
+ ```
169
+
170
+ ### 2. Constructor parameter
171
+
172
+ Pass the token directly (useful for apps that manage tokens themselves):
173
+
174
+ ```typescript
175
+ const client = new EsiClient({ accessToken: token });
176
+ ```
177
+
178
+ ### 3. Runtime update
179
+
180
+ Set or refresh the token after construction:
181
+
182
+ ```typescript
183
+ client.setAccessToken(newToken);
184
+ ```
185
+
186
+ ### Getting an EVE SSO token
187
+
188
+ 1. Register an application at [EVE Developers](https://developers.eveonline.com/)
189
+ 2. Set a callback URL and select the ESI scopes your app needs
190
+ 3. Implement the [OAuth2 flow](https://docs.esi.evetech.net/docs/sso/) to obtain an access token
191
+ 4. Access tokens expire — use the refresh token to get new ones
192
+
193
+ ### Automatic Token Refresh
194
+
195
+ EVE SSO access tokens expire after 20 minutes. Instead of manually tracking expiry, you can provide a refresh callback — the client will automatically call it on 401, update the token, and retry the request:
196
+
197
+ ```typescript
198
+ const client = new EsiClient({
199
+ accessToken: initialToken,
200
+ onTokenRefresh: async () => {
201
+ const response = await fetch('https://login.eveonline.com/v2/oauth/token', {
202
+ method: 'POST',
203
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
204
+ body: new URLSearchParams({
205
+ grant_type: 'refresh_token',
206
+ refresh_token: myRefreshToken,
207
+ client_id: myClientId,
208
+ }),
209
+ });
210
+ const { access_token } = await response.json();
211
+ return access_token;
212
+ },
213
+ });
214
+
215
+ // Requests now auto-refresh on 401 — no manual token management needed
216
+ const location = await client.location.getCharacterLocation(characterId);
217
+ ```
218
+
219
+ The token provider can also be set or changed at runtime:
220
+
221
+ ```typescript
222
+ client.setTokenProvider(myRefreshFunction);
223
+ client.setTokenProvider(undefined); // disable auto-refresh
224
+ ```
225
+
226
+ Key behaviors:
227
+
228
+ - Only retries **once** per request — if the refreshed token also gets a 401, the error is thrown
229
+ - **Concurrent coalescing** — if multiple requests hit 401 simultaneously, only one refresh call is made
230
+ - If the refresh callback throws (e.g., refresh token revoked), a `TOKEN_REFRESH_FAILED` error is raised
231
+ - Without a token provider, 401 errors throw immediately as before
232
+
233
+ ### Environment variables reference
234
+
235
+ | Variable | Description | Default |
236
+ | ------------------ | -------------------------------------------- | ------------------------- |
237
+ | `ESI_ACCESS_TOKEN` | EVE SSO access token | none |
238
+ | `ESI_CLIENT_ID` | User-Agent identifier | `esi-client` |
239
+ | `ESI_BASE_URL` | ESI API base URL | `https://esi.evetech.net` |
240
+ | `ESI_LOG_LEVEL` | Log level (`error`, `warn`, `info`, `debug`) | `warn` |
241
+
242
+ ## Available APIs
243
+
244
+ All clients are accessed as properties on the `EsiClient` instance. Authenticated endpoints require an access token.
245
+
246
+ | Client | Property | Auth | Examples |
247
+ | -------------- | ---------------------- | ---- | -------------------------------------------------------------------------------- |
248
+ | Alliance | `client.alliance` | Some | `getAlliances()`, `getAllianceById(id)` |
249
+ | Assets | `client.assets` | Yes | `getCharacterAssets(id)` |
250
+ | Calendar | `client.calendar` | Yes | `getCalendarEvents(id)` |
251
+ | Characters | `client.characters` | Some | `getCharacterPublicInfo(id)`, `getCharacterPortrait(id)` |
252
+ | Clones | `client.clones` | Yes | `getCharacterClones(id)` |
253
+ | Contacts | `client.contacts` | Yes | `getCharacterContacts(id)`, `postCharacterContacts(id, standing, contactIds)` |
254
+ | Contracts | `client.contracts` | Yes | `getCharacterContracts(id)` |
255
+ | Corporations | `client.corporations` | Some | `getCorporationInfo(id)`, `getCorporationMembers(id)` |
256
+ | Dogma | `client.dogma` | No | `getDogmaAttributes()`, `getDynamicItemInfo(typeId, itemId)` |
257
+ | Factions | `client.factions` | Some | `getFactionWarStats()` |
258
+ | Fittings | `client.fittings` | Yes | `getFittings(id)`, `createFitting(id, body)` |
259
+ | Fleets | `client.fleets` | Yes | `getFleetInformation(id)`, `getFleetMembers(id)` |
260
+ | Incursions | `client.incursions` | No | `getIncursions()` |
261
+ | Industry | `client.industry` | Some | `getCharacterIndustryJobs(id)` |
262
+ | Insurance | `client.insurance` | No | `getInsurancePrices()` |
263
+ | Killmails | `client.killmails` | Some | `getKillmail(id, hash)` |
264
+ | Location | `client.location` | Yes | `getCharacterLocation(id)` |
265
+ | Loyalty | `client.loyalty` | Yes | `getCharacterLoyaltyPoints(id)` |
266
+ | Mail | `client.mail` | Yes | `getCharacterMail(id)`, `sendMail(id, body)` |
267
+ | Market | `client.market` | Some | `getMarketPrices()`, `getMarketOrders(regionId)` |
268
+ | PI | `client.pi` | Yes | `getCharacterPlanets(id)` |
269
+ | Route | `client.route` | No | `getRoute(origin, destination)` |
270
+ | Search | `client.search` | Some | `search(characterId, query)` |
271
+ | Skills | `client.skills` | Yes | `getCharacterSkills(id)` |
272
+ | Sovereignty | `client.sovereignty` | No | `getSovereigntySystems()`, `getSovereigntyMap()` |
273
+ | Skyhooks | `client.skyhooks` | No | `getSovereigntyHubs()`, `getRaidableSkyhooks()` |
274
+ | Mercenary | `client.mercenary` | No | `getMercenaryDens()`, `getMercenaryTacticalOperations()` |
275
+ | Access Lists | `client.accessLists` | Yes | `getAccessList(id)` |
276
+ | Status | `client.status` | No | `getStatus()` |
277
+ | UI | `client.ui` | Yes | `setAutopilotWaypoint(destId, addToBeginning, clear)`, `openNewMailWindow(body)` |
278
+ | Universe | `client.universe` | Some | `getSystemById(id)`, `getTypeById(id)` |
279
+ | Wallet | `client.wallet` | Yes | `getCharacterWallet(id)` |
280
+ | Wars | `client.wars` | No | `getWars()`, `getWarById(id)` |
281
+ | Freelance Jobs | `client.freelanceJobs` | Some | `getFreelanceJobs()`, `getFreelanceJobById(id)` |
282
+ | Meta | `client.meta` | No | `getOpenApiJson()`, `getOpenApiYaml()` |
283
+
284
+ ## Runtime Response Validation
285
+
286
+ ESI.ts validates API responses at runtime using [Zod](https://zod.dev/) schemas. All GET endpoints have schemas — these are the endpoints that return data your application consumes, where a silent shape change from CCP would cause bugs. POST/PUT/DELETE mutations typically return `204 No Content` (no body to validate) or simple confirmation values, so schemas are omitted where there is nothing meaningful to validate.
287
+
288
+ Validation is **on by default**. Extra fields from ESI are preserved via `z.looseObject()` passthrough mode, so new fields added by CCP won't break your application — they flow through to your code untouched.
289
+
290
+ ```typescript
291
+ import {
292
+ EsiClient,
293
+ EsiValidationError,
294
+ isValidationError,
295
+ schemas,
296
+ } from '@lgriffin/esi.ts';
297
+
298
+ const client = new EsiClient();
299
+
300
+ // Validation happens automatically on every request
301
+ const character = await client.characters.getCharacterPublicInfo(12345);
302
+
303
+ // Disable validation globally if needed
304
+ const rawClient = new EsiClient({ validateResponse: false });
305
+
306
+ // Use schemas directly for your own validation
307
+ const result = schemas.CharacterInfoSchema.safeParse(someData);
308
+ if (result.success) {
309
+ console.log(result.data.name);
310
+ }
311
+ ```
312
+
313
+ ### Request Body Validation
314
+
315
+ For POST/PUT/DELETE endpoints, opt-in request body validation ensures outgoing payloads match the endpoint's `requestSchema` before the request is sent:
316
+
317
+ ```typescript
318
+ // Opt-in request body validation for POST/PUT/DELETE
319
+ const client = new EsiClient({ validateRequest: true });
320
+
321
+ // Throws EsiValidationError if the request body doesn't match the endpoint's requestSchema
322
+ await client.mail.sendMail(characterId, {
323
+ recipients: [{ recipient_id: 12345, recipient_type: 'character' }],
324
+ subject: 'Hello',
325
+ body: 'Message body',
326
+ });
327
+ ```
328
+
329
+ See [guides/RUNTIME-VALIDATION.md](guides/RUNTIME-VALIDATION.md) for the full guide on schemas, error handling, and extending schemas.
330
+
331
+ ## Caching
332
+
333
+ ETag caching is enabled by default. The client automatically:
334
+
335
+ 1. Stores ETag and response data on GET requests
336
+ 2. Sends `If-None-Match` on subsequent requests
337
+ 3. Returns cached data on `304 Not Modified`
338
+ 4. Parses `Cache-Control: max-age` from ESI for per-endpoint TTL
339
+ 5. Serves stale cached data when ESI returns 5xx errors
340
+ 6. Invalidates related GET caches when POST/PUT/DELETE requests are made
341
+
342
+ ```typescript
343
+ // Cache stats
344
+ const stats = client.getCacheStats();
345
+ console.log(`${stats.totalEntries}/${stats.maxEntries} entries cached`);
346
+
347
+ // Manual cache operations
348
+ client.clearCache();
349
+ client.updateCacheConfig({ maxEntries: 2000 });
350
+
351
+ // Disable caching entirely
352
+ const uncachedClient = new EsiClient({ enableETagCache: false });
353
+ ```
354
+
355
+ ### Spec-Aware Cache TTLs
356
+
357
+ The library reads `x-cache-age` from the ESI OpenAPI spec (126 of 195 endpoints). Within the TTL window, repeated GET requests return cached data with **zero HTTP calls** — not even a conditional GET.
358
+
359
+ This layers on top of ETag caching in three tiers:
360
+
361
+ 1. **Spec TTL** — data can't have changed yet, return cached data immediately
362
+ 2. **ETag conditional GET** — data might have changed, send `If-None-Match` to check
363
+ 3. **Full request** — no cache entry, fetch fresh data
364
+
365
+ ```typescript
366
+ const client = new EsiClient();
367
+
368
+ // First call — fetches from ESI
369
+ const alliances = await client.alliance.getAlliances();
370
+
371
+ // Second call within the next 3600s — returns cached data, zero HTTP calls
372
+ const same = await client.alliance.getAlliances();
373
+ ```
374
+
375
+ ## Batch Requests
376
+
377
+ Fetch data for multiple IDs with bounded concurrency using `batch()`, or chunk large POST payloads with `batchPost()`:
378
+
379
+ ```typescript
380
+ import { EsiClient } from '@lgriffin/esi.ts';
381
+
382
+ const client = new EsiClient();
383
+
384
+ // Fetch 500 type details with at most 10 concurrent requests
385
+ const result = await client.batch(
386
+ typeIds,
387
+ (id) => client.universe.getTypeById(id),
388
+ {
389
+ concurrency: 10,
390
+ onProgress: (done, total) => console.log(`${done}/${total}`),
391
+ },
392
+ );
393
+
394
+ // result.results: Map<number, T> — successful responses
395
+ // result.errors: Map<number, Error> — failed requests
396
+ console.log(`${result.results.size} succeeded, ${result.errors.size} failed`);
397
+ ```
398
+
399
+ For POST endpoints that accept arrays (e.g., `postUniverseNames` with a 1000-ID limit), `batchPost` auto-chunks and concatenates:
400
+
401
+ ```typescript
402
+ const allNames = await client.batchPost(
403
+ largeIdArray,
404
+ (chunk) => client.universe.postUniverseNames(chunk),
405
+ 1000, // chunk size
406
+ );
407
+ ```
408
+
409
+ ## Streaming Pagination
410
+
411
+ For large paginated endpoints (market orders, contracts, assets), streaming yields one page at a time via `AsyncGenerator` instead of eagerly fetching all pages into memory:
412
+
413
+ ```typescript
414
+ import { EsiClient } from '@lgriffin/esi.ts';
415
+
416
+ const client = new EsiClient();
417
+
418
+ // Stream all market orders in The Forge, page by page
419
+ for await (const page of client.market.streamMarketOrders(10000002)) {
420
+ console.log(
421
+ `Page ${page.page}/${page.totalPages}: ${page.data.length} orders`,
422
+ );
423
+
424
+ // Process each order as it arrives
425
+ for (const order of page.data) {
426
+ if (order.is_buy_order && order.price > 1_000_000) {
427
+ console.log(`High-value buy: ${order.type_id} @ ${order.price} ISK`);
428
+ }
429
+ }
430
+
431
+ // Early termination — stops fetching remaining pages
432
+ if (page.page >= 3) break;
433
+ }
434
+ ```
435
+
436
+ 21 domain clients expose 73+ streaming methods. `BaseEsiClient.streamEndpoint()` is also public as an escape hatch for any paginated endpoint not yet wrapped with a convenience method.
437
+
438
+ Available streaming methods (representative selection):
439
+
440
+ - **MarketClient** — `streamMarketOrders`, `streamMarketTypes`, `streamCharacterOrderHistory`, `streamCorporationOrders`, `streamCorporationOrderHistory`, `streamMarketOrdersInStructure`
441
+ - **CorporationsClient** — `streamCorporationMembers`, `streamCorporationStructures`, `streamCorporationBlueprints`, + 14 more
442
+ - **CharacterClient** — `streamCharacterBlueprints`, `streamCharacterNotifications`, `streamCharacterStandings`, + 5 more
443
+ - **ContractsClient** — `streamPublicContracts`, `streamCharacterContracts`, `streamCorporationContracts`
444
+ - **WalletClient** — `streamCharacterWalletJournal`, `streamCorporationWalletJournal`, `streamCharacterWalletTransactions`
445
+ - **IndustryClient** — `streamCorporationIndustryJobs`, `streamCorporationMiningObservers`, + 6 more
446
+ - **ContactsClient** — `streamAllianceContacts`, `streamCharacterContacts`, `streamCorporationContacts`, + 3 more
447
+ - **AssetsClient** — `streamCharacterAssets`, `streamCorporationAssets`
448
+ - **KillmailsClient** — `streamCharacterRecentKillmails`, `streamCorporationRecentKillmails`
449
+ - **MailClient** — `streamCharacterMail`, `streamCharacterMailLabels`
450
+ - **FleetsClient** — `streamFleetMembers`, `streamFleetWings`
451
+ - **CalendarClient** — `streamCalendarEvents`
452
+ - **FittingsClient** — `streamCharacterFittings`
453
+ - **SkillsClient** — `streamCharacterSkillQueue`
454
+ - **LoyaltyClient** — `streamCorporationLoyaltyStoreOffers`
455
+ - **BookmarksClient** — `streamCharacterBookmarks`, `streamCorporationBookmarks`
456
+ - **ClonesClient** — `streamCharacterImplants`
457
+ - **PIClient** — `streamCharacterPlanets`
458
+ - **WarsClient** — `streamWars`
459
+ - **FactionWarfareClient** — `streamFactionWarfareStats`
460
+ - **AllianceClient** — `streamAllianceCorporations`
461
+
462
+ Try it: `npm run example:streaming`
463
+
464
+ ## Cursor-based Pagination
465
+
466
+ Newer ESI routes (Freelance Jobs, and future routes) use cursor-based pagination with opaque `before`/`after` tokens in the response body. See the [ESI blog post](https://developers.eveonline.com/blog/changing-pagination-turning-a-new-page) for background.
467
+
468
+ ```typescript
469
+ import { EsiClient, fetchAllCursorPages } from '@lgriffin/esi.ts';
470
+
471
+ const client = new EsiClient();
472
+
473
+ // Fetch first page — returns { cursor: { before, after }, freelance_jobs: [...] }
474
+ const page = await client.freelanceJobs.getFreelanceJobs();
475
+ console.log(page.freelance_jobs); // job records
476
+ console.log(page.cursor.after); // opaque token for next page
477
+
478
+ // Fetch next page using the cursor
479
+ const nextPage = await client.freelanceJobs.getFreelanceJobs(
480
+ undefined,
481
+ page.cursor.after,
482
+ );
483
+
484
+ // Auto-fetch all pages in one call
485
+ const allJobs = await fetchAllCursorPages(
486
+ (before, after) => client.freelanceJobs.getFreelanceJobs(before, after),
487
+ (response) => response.freelance_jobs,
488
+ (response) => response.cursor,
489
+ );
490
+
491
+ // Authenticated endpoints — character/corporation freelance jobs
492
+ const authedClient = new EsiClient({ accessToken: 'your-token' });
493
+ const myJobs =
494
+ await authedClient.freelanceJobs.getCharacterFreelanceJobs(characterId);
495
+ const corpJobs =
496
+ await authedClient.freelanceJobs.getCorporationFreelanceJobs(corporationId);
497
+ ```
498
+
499
+ **Polling for changes** — cursor tokens persist across sessions, so you can save the last `after` token and poll later to get only records that changed:
500
+
501
+ ```typescript
502
+ // After initial scan, save the final cursor
503
+ let savedCursor = lastPage.cursor.after;
504
+
505
+ // Later: check for updates (hours, days, or weeks later)
506
+ const updates = await client.freelanceJobs.getFreelanceJobs(
507
+ undefined,
508
+ savedCursor,
509
+ );
510
+ if (updates.freelance_jobs.length > 0) {
511
+ // Process changed records — duplicates are expected for modified records
512
+ savedCursor = updates.cursor.after;
513
+ }
514
+ ```
515
+
516
+ Key points:
517
+
518
+ - Cursor tokens are **opaque strings** — never parse or validate them
519
+ - An **empty result array** signals the end of the dataset (not a short page)
520
+ - **Duplicates across pages** are expected when records are modified between requests
521
+ - Existing offset-based routes (`getMarketOrders`, etc.) are unchanged
522
+
523
+ ## Generated Types
524
+
525
+ The library includes TypeScript interfaces generated directly from the ESI OpenAPI 3.1 spec, available as the `EsiSpec` namespace. These are guaranteed to match the live spec and complement the hand-written types:
526
+
527
+ ```typescript
528
+ import { EsiSpec } from '@lgriffin/esi.ts';
529
+
530
+ // Generated type — uses OpenAPI schema names (v7.0.0+)
531
+ const order: EsiSpec.MarketsRegionIdOrdersGet = {
532
+ order_id: 123,
533
+ type_id: 34,
534
+ price: 5.5,
535
+ volume_remain: 1000,
536
+ volume_total: 5000,
537
+ is_buy_order: false,
538
+ // ...
539
+ };
540
+ ```
541
+
542
+ To regenerate types from the latest ESI spec:
543
+
544
+ ```bash
545
+ npm run generate:types # fetches OpenAPI spec, generates 161 interfaces + cache TTL map + rate limit groups + scope map
546
+ npm run validate:esi # reports type drift between hand-written and generated types
547
+ ```
548
+
549
+ ## ESI Scopes
550
+
551
+ The library includes a generated scope-to-endpoint mapping extracted from the ESI OpenAPI spec. Use it to check which OAuth scopes an endpoint requires before making a request:
552
+
553
+ ```typescript
554
+ import { esiEndpointScopes, EsiScope } from '@lgriffin/esi.ts';
555
+
556
+ // Look up scopes for a specific endpoint
557
+ const walletScopes = esiEndpointScopes['GET:characters/{character_id}/wallet'];
558
+ // → ['esi-wallet.read_character_wallet.v1']
559
+
560
+ // Check if an endpoint requires auth
561
+ const isPublic = !esiEndpointScopes['GET:universe/types/{type_id}'];
562
+ // → true (public endpoint, no scopes needed)
563
+
564
+ // Type-safe scope values
565
+ const scope: EsiScope = 'esi-assets.read_assets.v1';
566
+ ```
567
+
568
+ ## Error Handling
569
+
570
+ API errors throw `EsiError` with `statusCode`, `message`, and `url` properties:
571
+
572
+ ```typescript
573
+ import {
574
+ EsiError,
575
+ TimeoutError,
576
+ EsiValidationError,
577
+ isTimeout,
578
+ isRetryable,
579
+ isValidationError,
580
+ } from '@lgriffin/esi.ts';
581
+
582
+ try {
583
+ const alliance = await client.alliance.getAllianceById(99999999);
584
+ console.log('Alliance:', alliance.name);
585
+ } catch (err) {
586
+ if (isValidationError(err)) {
587
+ console.log('Response validation failed:', err.validationError);
588
+ } else if (isTimeout(err)) {
589
+ console.log(`Request timed out after ${err.timeoutMs}ms`);
590
+ } else if (err instanceof EsiError) {
591
+ console.log(`ESI error ${err.statusCode}: ${err.message}`);
592
+ console.log(`Retryable: ${err.retryable}`);
593
+ }
594
+ }
595
+ ```
596
+
597
+ - **204 No Content** — returns `undefined` (valid for DELETE/POST actions)
598
+ - **304 Not Modified** — handled internally, returns cached data
599
+ - **4xx/5xx** — throws `EsiError`
600
+ - **5xx with cache** — returns stale cached data instead of throwing
601
+ - **Timeout** — throws `TimeoutError` (extends `EsiError` with `statusCode: 0` and `timeoutMs`)
602
+ - **Retryable errors** — `EsiError.retryable` returns `true` for 502, 503, 504, 420, 429, and timeouts
603
+ - **Validation errors** — throws `EsiValidationError` (extends `EsiError`) when response data doesn't match the expected Zod schema
604
+
605
+ ## Response Metadata
606
+
607
+ Use `withMetadata()` to get response headers, cache status, rate limit info, and timing alongside the data:
608
+
609
+ ```typescript
610
+ const metaClient = client.alliance.withMetadata();
611
+ const result = await metaClient.getAllianceById(99000001);
612
+
613
+ console.log(result.data.name); // "Goonswarm Federation"
614
+ console.log(result.meta.fromCache); // true if served from cache
615
+ console.log(result.meta.cacheHitType); // 'spec-ttl' | 'etag-304' | 'stale-on-error'
616
+ console.log(result.meta.responseTimeMs); // milliseconds
617
+ console.log(result.meta.rateLimit); // { remaining, limit, used, group }
618
+ console.log(result.meta.requestId); // ESI request ID for debugging
619
+ ```
620
+
621
+ The `meta` object includes:
622
+
623
+ | Field | Type | Description |
624
+ | ---------------- | ------------------------ | ------------------------------------------------- |
625
+ | `headers` | `Record<string, string>` | Raw response headers |
626
+ | `fromCache` | `boolean` | Whether data was served from cache |
627
+ | `stale` | `boolean` | Whether cached data is stale (5xx fallback) |
628
+ | `cacheHitType` | `string?` | `'spec-ttl'`, `'etag-304'`, or `'stale-on-error'` |
629
+ | `rateLimit` | `RateLimitMeta?` | Rate limit status from ESI headers |
630
+ | `responseTimeMs` | `number?` | Request duration in milliseconds |
631
+ | `requestId` | `string?` | ESI request ID |
632
+ | `warning` | `object?` | ESI deprecation warning |
633
+
634
+ ## Rate Limiting
635
+
636
+ ESI.ts automatically manages rate limiting using ESI's per-group token bucket system. The 36 rate limit groups from the ESI OpenAPI spec are extracted at build time, so each group (e.g., `market-order`, `char-notification`) gets its own independent bucket. A burst of market requests won't starve unrelated endpoints.
637
+
638
+ Rate limiting works out of the box with no configuration. For multi-character applications, enable per-user bucketing:
639
+
640
+ ```typescript
641
+ import { EsiClient } from '@lgriffin/esi.ts';
642
+
643
+ const client = new EsiClient({
644
+ rateLimiterConfig: {
645
+ userKeyExtractor: (headers) => headers['authorization'] ?? 'anon',
646
+ },
647
+ });
648
+ ```
649
+
650
+ Monitor rate limit status per group:
651
+
652
+ ```typescript
653
+ const limiter = client.getRateLimiter();
654
+
655
+ // Worst-case across all groups (backward-compatible)
656
+ const status = limiter.getStatus();
657
+ console.log(status.remaining, status.limit, status.group);
658
+
659
+ // Specific group
660
+ const marketStatus = limiter.getGroupStatus('market-order');
661
+ console.log(marketStatus?.remaining); // tokens remaining in this group
662
+
663
+ // All active groups
664
+ const all = limiter.getAllGroupStatuses();
665
+ for (const [group, info] of all) {
666
+ console.log(`${group}: ${info.remaining}/${info.limit}`);
667
+ }
668
+
669
+ // Check if a specific group is blocked
670
+ console.log(limiter.isBlocked('char-notification')); // true if 429'd
671
+ ```
672
+
673
+ ## Lightweight Clients
674
+
675
+ All three client creation patterns (`EsiClient`, `CustomEsiClient`, `EsiApiFactory`) now get identical middleware defaults (cache, request deduplication, rate limiter) thanks to `configureApiClient()`. Previously `CustomEsiClient` and `EsiApiFactory` only configured the rate limiter.
676
+
677
+ If you only need a subset of APIs, use `CustomEsiClient` or `EsiClientBuilder` to load only what you need:
678
+
679
+ ```typescript
680
+ import { EsiClientBuilder } from '@lgriffin/esi.ts';
681
+
682
+ const client = new EsiClientBuilder()
683
+ .addClients(['market', 'universe', 'characters'])
684
+ .withClientId('my-trading-bot')
685
+ .withAccessToken('your-token')
686
+ .build();
687
+
688
+ const prices = await client.market?.getMarketPrices();
689
+ const system = await client.universe?.getSystemById(30000142);
690
+ ```
691
+
692
+ Or create standalone single-API clients:
693
+
694
+ ```typescript
695
+ import { EsiApiFactory } from '@lgriffin/esi.ts';
696
+
697
+ const marketClient = EsiApiFactory.createMarketClient({
698
+ clientId: 'price-checker',
699
+ });
700
+ const prices = await marketClient.getMarketPrices();
701
+ ```
702
+
703
+ ## Endpoint Coverage
704
+
705
+ All 208 endpoint definitions have been validated against live Tranquility using the **OpenAPI 3.1 spec** — 194 from the public ESI spec plus 14 for newer EVE features. 206 endpoints are exercisable (2 mercenary den endpoints await CCP deployment). Full output is captured in [`openapi.output.md`](openapi.output.md).
706
+
707
+ | Category | Endpoints | Method |
708
+ | --------------------------- | --------- | -------------------------------------------------- |
709
+ | Public GETs | 78 | 43 runnable example scripts with captured output |
710
+ | Authenticated GETs | 72 | Example scripts + live testing with EVE SSO tokens |
711
+ | Contacts (POST/PUT/DELETE) | 3 | Live create/edit/delete lifecycle |
712
+ | Fittings (POST/DELETE) | 2 | Live create/delete lifecycle |
713
+ | Mail (POST/PUT/DELETE) | 5 | Live send/label/metadata/delete lifecycle |
714
+ | UI (POST) | 5 | Live testing with EVE client running |
715
+ | Calendar (PUT) | 1 | Live RSVP to event |
716
+ | Fleet (GET/POST/PUT/DELETE) | 14 | Live fleet with fleet commander + squad members |
717
+ | Assets POST | 3 | Live asset location/name queries |
718
+ | CSPA (POST) | 1 | Live charge cost calculation |
719
+ | Dogma dynamic (GET) | 1 | Live mutaplasmid (Abyssal) item query |
720
+ | Universe POST helpers | 3 | Live name resolution and affiliation |
721
+ | Freelance Jobs (GET) | 4 | Live queries (graceful 404 for no active jobs) |
722
+
723
+ ## Examples
724
+
725
+ 43 runnable examples are in the `examples/` directory.
726
+
727
+ ### Public Endpoints (no auth needed)
728
+
729
+ ```bash
730
+ npm run example:status # Server status — quickest smoke test
731
+ npm run example:character # Character public info, portrait, corporation
732
+ npm run example:universe # Solar system, constellation, region, station
733
+ npm run example:market # Average prices + Tritanium price history
734
+ npm run example:alliance # Alliance info + member corporations
735
+ npm run example:route # Jita-to-Amarr route with system names
736
+ npm run example:wars # Recent wars with aggressor/defender details
737
+ npm run example:sovereignty # Nullsec sovereignty map + active campaigns
738
+ npm run example:industry # Industry facilities, cost indices, insurance
739
+ npm run example:incursions # Active incursions + faction warfare stats
740
+ npm run example:dogma # Item type details + dogma attributes
741
+ npm run example:contracts # Public region contracts + auction bids/items
742
+ npm run example:rate-limiting # Rate limiter & pagination demonstration
743
+ npm run example:cursor-pagination # Freelance Jobs with cursor pagination
744
+ npm run example:streaming # Streaming pagination for large datasets
745
+ npm run example:token-refresh # Automatic token refresh on 401
746
+ npm run example:universe-encyclopedia # Ancestries, bloodlines, races, celestials
747
+ npm run example:dogma-meta-sov # Dogma effects, sovereignty, meta endpoint
748
+ npm run example:faction-details # Faction warfare leaderboards and stats
749
+ ```
750
+
751
+ ### Authenticated Endpoints (require ESI_ACCESS_TOKEN)
752
+
753
+ ```bash
754
+ npm run example # Full character profile assembly
755
+ npm run example:wallet # Wallet balance, journal, transactions
756
+ npm run example:skills # Trained skills, queue, attributes
757
+ npm run example:assets # Asset inventory with bulk name lookup
758
+ npm run example:killmails # Recent killmails + full details
759
+ npm run example:fleet # Fleet info, members, wing/squad structure
760
+ npm run example:mail # Inbox headers, labels, mailing lists
761
+ npm run example:location # Current system, online status, ship
762
+ npm run example:fittings # Saved fittings + clone state + implants
763
+ npm run example:contacts # Contact list with standings + labels
764
+ npm run example:character-details # Blueprints, roles, standings, medals
765
+ npm run example:corporation-details # Corp members, divisions, structures
766
+ npm run example:calendar-search # Calendar events + character search
767
+ npm run example:loyalty-pi # Loyalty points + planetary interaction
768
+ npm run example:industry-mining # Industry jobs + mining ledger
769
+ npm run example:market-orders # Character/corp market orders
770
+ npm run example:corp-contracts-wallet # Corp contracts, contacts, wallets
771
+ ```
772
+
773
+ ### Write Operations (require specific scopes + caution)
774
+
775
+ ```bash
776
+ npm run example:write-ops # Contacts, fittings, mail, UI lifecycle tests
777
+ npm run example:universe-posts # Name resolution + character affiliation (public)
778
+ npm run example:freelance-jobs # Freelance job queries
779
+ ```
780
+
781
+ ### Parallel Requests
782
+
783
+ ```typescript
784
+ const [character, portrait, corp] = await Promise.all([
785
+ client.characters.getCharacterPublicInfo(characterId),
786
+ client.characters.getCharacterPortrait(characterId),
787
+ client.corporations.getCorporationInfo(corporationId),
788
+ ]);
789
+
790
+ console.log(`${character.name} [${corp.ticker}]`);
791
+ ```
792
+
793
+ ### Market Analysis
794
+
795
+ ```typescript
796
+ const [orders, history] = await Promise.all([
797
+ client.market.getMarketOrders(regionId),
798
+ client.market.getMarketHistory(regionId, typeId),
799
+ ]);
800
+
801
+ const buyOrders = orders.filter((o) => o.is_buy_order);
802
+ const sellOrders = orders.filter((o) => !o.is_buy_order);
803
+
804
+ console.log(`Best buy: ${Math.max(...buyOrders.map((o) => o.price))}`);
805
+ console.log(`Best sell: ${Math.min(...sellOrders.map((o) => o.price))}`);
806
+ ```
807
+
808
+ ## Resource Management
809
+
810
+ Always call `shutdown()` when you're done to clean up cache timers:
811
+
812
+ ```typescript
813
+ const client = new EsiClient();
814
+ try {
815
+ const status = await client.status.getStatus();
816
+ console.log(status.server_version);
817
+ } finally {
818
+ await client.shutdown();
819
+ }
820
+ ```
821
+
822
+ ## Testing
823
+
824
+ ESI.ts has a comprehensive multi-tier testing strategy with 95+ suites and 3,800+ tests:
825
+
826
+ | Tier | Tests | Purpose |
827
+ | -------------------------- | ---------------- | ------------------------------------------------------------------ |
828
+ | **TDD unit tests** | 81 files | Every client method, endpoint path, query param, and body format |
829
+ | **BDD scenario tests** | 40 feature files | Behavioral specifications in Gherkin (Given/When/Then) |
830
+ | **Mocked integration** | Full suite | Cross-layer request flow with jest-fetch-mock |
831
+ | **Live smoke tests** | 43 examples | Every endpoint against live Tranquility |
832
+ | **ESI spec contract** | 15 tests | Endpoint definitions validated against live OpenAPI spec |
833
+ | **Deep contract tests** | 8 categories | Path params, query params, body, auth, schemas, pagination vs spec |
834
+ | **Property-based fuzzing** | 601 tests | fast-check fuzzing of validation, URL construction, Zod schemas |
835
+ | **Mutation testing** | Stryker | Validates test suite kills code mutants |
836
+ | **Type-level tests** | tsd | Consumer API type correctness via tsd |
837
+ | **Gated auth tests** | 33 tests | Authenticated endpoints with real tokens |
838
+ | **Construction parity** | Per-surface | Verifies all client surfaces get identical middleware defaults |
839
+ | **Spec-alignment** | Type assertions | Ensures hand-written types align with generated OpenAPI types |
840
+
841
+ ```bash
842
+ npm test # Unit + BDD tests (95+ suites, 3,800+ tests)
843
+ npm run coverage # Tests with coverage report (thresholds enforced)
844
+ npm run bdd # BDD scenario tests only
845
+ npm run contract # Contract tests (skipped without ESI_LIVE_TESTS=true)
846
+ npm run fuzz # Property-based fuzz tests (601 tests)
847
+ npm run mutation # Mutation testing (Stryker)
848
+ npm run benchmark # Performance benchmark tests
849
+ npm run test:types # tsd consumer type tests
850
+ ```
851
+
852
+ Coverage thresholds are enforced in CI: branches 80%, functions 75%, lines 90%, statements 90%.
853
+
854
+ See [guides/TESTING.md](guides/TESTING.md) for the full testing guide, and [guides/ARCHITECTURE.md](guides/ARCHITECTURE.md) for architecture diagrams.
855
+
856
+ ## Development
857
+
858
+ ### Prerequisites
859
+
860
+ - Node.js 18+
861
+ - npm
862
+
863
+ ### Code Quality Tools
864
+
865
+ The project uses a comprehensive suite of static analysis and code quality tools:
866
+
867
+ | Tool | Purpose | Command |
868
+ | ------------------------------------------------------------------------------------ | ------------------------------------------------------- | ------------------------------ |
869
+ | [ESLint](https://eslint.org/) | Linting with TypeScript, security, and code smell rules | `npm run lint` |
870
+ | [Prettier](https://prettier.io/) | Code formatting | `npm run format:check` |
871
+ | [knip](https://knip.dev/) | Dead code and unused export detection | `npm run knip` |
872
+ | [eslint-plugin-security](https://github.com/eslint-community/eslint-plugin-security) | Security anti-pattern detection | Integrated into `npm run lint` |
873
+ | [eslint-plugin-sonarjs](https://github.com/SonarSource/eslint-plugin-sonarjs) | Cognitive complexity and code smell detection | Integrated into `npm run lint` |
874
+ | [husky](https://typicode.github.io/husky/) | Git pre-commit hooks | Automatic on commit |
875
+ | [lint-staged](https://github.com/lint-staged/lint-staged) | Run linters on staged files only | Automatic on commit |
876
+ | [Redocly CLI](https://redocly.com/docs/cli/) | OpenAPI spec validation and linting | `npm run validate:spec` |
877
+
878
+ ### Available Scripts
879
+
880
+ ```bash
881
+ # Development
882
+ npm run build # Compile TypeScript
883
+ npm run lint # Run ESLint
884
+ npm run lint:fix # Run ESLint with auto-fix
885
+ npm run format # Format code with Prettier
886
+ npm run format:check # Check formatting without modifying
887
+
888
+ # Testing
889
+ npm test # Unit tests (95+ suites, 3,800+ tests)
890
+ npm run test:all # Unit + BDD + integration + fuzz + type tests
891
+ npm run coverage # Tests with coverage report (thresholds enforced)
892
+ npm run bdd # BDD scenario tests
893
+ npm run contract:live # Deep contract tests against live ESI spec
894
+ npm run fuzz # Property-based fuzz tests (fast-check)
895
+ npm run mutation # Mutation testing (Stryker)
896
+ npm run benchmark # Performance benchmark tests
897
+ npm run test:types # Consumer type tests (tsd)
898
+ npm run mock:esi # Start Prism mock ESI server on port 4010
899
+
900
+ # Static Analysis
901
+ npm run knip # Detect dead code and unused exports
902
+ npm run validate:esi # Validate endpoints against live ESI OpenAPI spec
903
+ npm run validate:spec # Lint ESI OpenAPI spec with Redocly (structural + best practices)
904
+ npm run validate:auth-scopes # Auth/scope cross-validation
905
+ npm run schema:drift # Schema drift detection (hand-written vs OpenAPI spec)
906
+ npm run validate # Run all checks: lint, format, build, coverage, knip
907
+ npm run generate:types # Regenerate TypeScript interfaces from ESI OpenAPI spec
908
+ npm run generate:okf # Generate OKF knowledge bundle from ESI OpenAPI spec
909
+
910
+ # Documentation
911
+ npm run docs # Generate TypeDoc API documentation
912
+ npm run docs:serve # Serve docs locally on port 8080
913
+ ```
914
+
915
+ ### ESI Endpoint Validation
916
+
917
+ To verify that the codebase endpoint definitions match the live ESI OpenAPI spec:
918
+
919
+ ```bash
920
+ npm run validate:esi
921
+ ```
922
+
923
+ This fetches the ESI OpenAPI spec and reports:
924
+
925
+ - Endpoints in the codebase that are no longer in the ESI spec
926
+ - Endpoints in the ESI spec that the codebase doesn't cover
927
+ - HTTP method mismatches between codebase and spec
928
+
929
+ ### Pre-commit Hooks
930
+
931
+ The project uses husky with lint-staged to run ESLint and Prettier on staged files before each commit. This is set up automatically when you run `npm install`.
932
+
933
+ ### CI/CD
934
+
935
+ Every pull request runs the full validation suite:
936
+
937
+ - ESLint (with security and sonarjs plugins)
938
+ - Prettier formatting check
939
+ - TypeScript compilation
940
+ - Generated types staleness check (regenerates from live ESI OpenAPI spec and verifies no diff)
941
+ - Unit tests across Node.js 18, 20, and 22
942
+ - BDD scenario tests
943
+ - Coverage threshold enforcement (branches: 80%, functions: 75%, lines: 90%, statements: 90%)
944
+ - Auth/scopes cross-validation
945
+ - Spec-alignment type assertions
946
+ - Schema drift detection
947
+ - Mutation testing (Stryker)
948
+ - Dead code detection via knip
949
+ - npm security audit
950
+
951
+ See [.github/workflows/README.md](.github/workflows/README.md) for full workflow details.
952
+
953
+ ## Contributing
954
+
955
+ 1. Fork the repository
956
+ 2. Create a feature branch
957
+ 3. Write tests for your changes
958
+ 4. Run `npm run validate` to check everything passes
959
+ 5. Open a Pull Request
960
+
961
+ ## License
962
+
963
+ GPL-3.0-or-later - see the [LICENSE](LICENSE) file for details.
964
+
965
+ ---
966
+
967
+ **o7**