@lgriffin/esi.ts 9.6.0 → 10.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 (259) hide show
  1. package/CHANGELOG.md +787 -388
  2. package/LICENSE +26 -26
  3. package/README.md +974 -1030
  4. package/dist/EsiClient.d.ts +4 -0
  5. package/dist/EsiClient.d.ts.map +1 -1
  6. package/dist/EsiClientBuilder.d.ts.map +1 -1
  7. package/dist/auth/EsiTokenManager.d.ts +200 -0
  8. package/dist/auth/EsiTokenManager.d.ts.map +1 -0
  9. package/dist/auth/EveSsoClient.d.ts +88 -0
  10. package/dist/auth/EveSsoClient.d.ts.map +1 -0
  11. package/dist/auth/errors.d.ts +42 -0
  12. package/dist/auth/errors.d.ts.map +1 -0
  13. package/dist/auth/index.d.ts +14 -0
  14. package/dist/auth/index.d.ts.map +1 -0
  15. package/dist/auth/jwt.d.ts +43 -0
  16. package/dist/auth/jwt.d.ts.map +1 -0
  17. package/dist/auth/pkce.d.ts +21 -0
  18. package/dist/auth/pkce.d.ts.map +1 -0
  19. package/dist/auth/storage/FileTokenStorage.d.ts +44 -0
  20. package/dist/auth/storage/FileTokenStorage.d.ts.map +1 -0
  21. package/dist/auth/storage/MemoryTokenStorage.d.ts +18 -0
  22. package/dist/auth/storage/MemoryTokenStorage.d.ts.map +1 -0
  23. package/dist/auth/types.d.ts +42 -0
  24. package/dist/auth/types.d.ts.map +1 -0
  25. package/dist/chunk-3ZI5A37L.mjs +1065 -0
  26. package/dist/chunk-3ZI5A37L.mjs.map +1 -0
  27. package/dist/chunk-7A72QFDF.mjs +416 -0
  28. package/dist/chunk-7A72QFDF.mjs.map +1 -0
  29. package/dist/chunk-BBSHKVPI.js +71 -0
  30. package/dist/chunk-BBSHKVPI.js.map +1 -0
  31. package/dist/chunk-HPSACXX3.js +416 -0
  32. package/dist/chunk-HPSACXX3.js.map +1 -0
  33. package/dist/chunk-JIW4BXLP.mjs +17 -0
  34. package/dist/chunk-JIW4BXLP.mjs.map +1 -0
  35. package/dist/chunk-MSJHIJXA.js +1065 -0
  36. package/dist/chunk-MSJHIJXA.js.map +1 -0
  37. package/dist/chunk-PZ5AY32C.js +10 -0
  38. package/dist/chunk-PZ5AY32C.js.map +1 -0
  39. package/dist/chunk-R7D7M2LQ.mjs +71 -0
  40. package/dist/chunk-R7D7M2LQ.mjs.map +1 -0
  41. package/dist/chunk-SAQBXXTM.js +2614 -0
  42. package/dist/chunk-SAQBXXTM.js.map +1 -0
  43. package/dist/chunk-VZS32UQ3.mjs +2614 -0
  44. package/dist/chunk-VZS32UQ3.mjs.map +1 -0
  45. package/dist/clients/AllianceClient.d.ts +2 -0
  46. package/dist/clients/AllianceClient.d.ts.map +1 -1
  47. package/dist/clients/AssetsClient.d.ts +2 -0
  48. package/dist/clients/AssetsClient.d.ts.map +1 -1
  49. package/dist/clients/BaseEsiClient.d.ts +1 -0
  50. package/dist/clients/BaseEsiClient.d.ts.map +1 -1
  51. package/dist/clients/CalendarClient.d.ts +2 -0
  52. package/dist/clients/CalendarClient.d.ts.map +1 -1
  53. package/dist/clients/CharacterClient.d.ts +8 -0
  54. package/dist/clients/CharacterClient.d.ts.map +1 -1
  55. package/dist/clients/ClonesClient.d.ts +1 -0
  56. package/dist/clients/ClonesClient.d.ts.map +1 -1
  57. package/dist/clients/ContactsClient.d.ts +6 -0
  58. package/dist/clients/ContactsClient.d.ts.map +1 -1
  59. package/dist/clients/ContractsClient.d.ts +14 -8
  60. package/dist/clients/ContractsClient.d.ts.map +1 -1
  61. package/dist/clients/CorporationProjectsClient.d.ts +22 -12
  62. package/dist/clients/CorporationProjectsClient.d.ts.map +1 -1
  63. package/dist/clients/CorporationsClient.d.ts +17 -0
  64. package/dist/clients/CorporationsClient.d.ts.map +1 -1
  65. package/dist/clients/FactionClient.d.ts +4 -4
  66. package/dist/clients/FactionClient.d.ts.map +1 -1
  67. package/dist/clients/FittingsClient.d.ts +1 -0
  68. package/dist/clients/FittingsClient.d.ts.map +1 -1
  69. package/dist/clients/FleetClient.d.ts +2 -0
  70. package/dist/clients/FleetClient.d.ts.map +1 -1
  71. package/dist/clients/FreelanceJobsClient.d.ts +3 -3
  72. package/dist/clients/FreelanceJobsClient.d.ts.map +1 -1
  73. package/dist/clients/IndustryClient.d.ts +11 -3
  74. package/dist/clients/IndustryClient.d.ts.map +1 -1
  75. package/dist/clients/KillmailsClient.d.ts +2 -0
  76. package/dist/clients/KillmailsClient.d.ts.map +1 -1
  77. package/dist/clients/LoyaltyClient.d.ts +2 -0
  78. package/dist/clients/LoyaltyClient.d.ts.map +1 -1
  79. package/dist/clients/MailClient.d.ts +8 -3
  80. package/dist/clients/MailClient.d.ts.map +1 -1
  81. package/dist/clients/MarketClient.d.ts +6 -0
  82. package/dist/clients/MarketClient.d.ts.map +1 -1
  83. package/dist/clients/MetaClient.d.ts +3 -2
  84. package/dist/clients/MetaClient.d.ts.map +1 -1
  85. package/dist/clients/PiClient.d.ts +2 -0
  86. package/dist/clients/PiClient.d.ts.map +1 -1
  87. package/dist/clients/SkillsClient.d.ts +1 -0
  88. package/dist/clients/SkillsClient.d.ts.map +1 -1
  89. package/dist/clients/WalletClient.d.ts +7 -3
  90. package/dist/clients/WalletClient.d.ts.map +1 -1
  91. package/dist/clients/WarsClient.d.ts +2 -0
  92. package/dist/clients/WarsClient.d.ts.map +1 -1
  93. package/dist/core/ApiClient.d.ts +8 -0
  94. package/dist/core/ApiClient.d.ts.map +1 -1
  95. package/dist/core/ApiClientBuilder.d.ts +3 -1
  96. package/dist/core/ApiClientBuilder.d.ts.map +1 -1
  97. package/dist/core/ApiRequestHandler.d.ts.map +1 -1
  98. package/dist/core/BatchRequestHandler.d.ts.map +1 -1
  99. package/dist/core/RequestDeduplicator.d.ts +2 -0
  100. package/dist/core/RequestDeduplicator.d.ts.map +1 -1
  101. package/dist/core/RetryStrategy.d.ts +2 -0
  102. package/dist/core/RetryStrategy.d.ts.map +1 -1
  103. package/dist/core/cache/ETagCacheManager.d.ts +4 -0
  104. package/dist/core/cache/ETagCacheManager.d.ts.map +1 -1
  105. package/dist/core/circuitBreaker/CircuitBreaker.d.ts +3 -0
  106. package/dist/core/circuitBreaker/CircuitBreaker.d.ts.map +1 -1
  107. package/dist/core/configureApiClient.d.ts.map +1 -1
  108. package/dist/core/constants.d.ts +2 -2
  109. package/dist/core/constants.d.ts.map +1 -1
  110. package/dist/core/endpoints/EndpointDefinition.d.ts +2 -2
  111. package/dist/core/endpoints/EndpointDefinition.d.ts.map +1 -1
  112. package/dist/core/endpoints/calendarEndpoints.d.ts +7 -7
  113. package/dist/core/endpoints/characterEndpoints.d.ts +2 -2
  114. package/dist/core/endpoints/cloneEndpoints.d.ts +2 -2
  115. package/dist/core/endpoints/contractEndpoints.d.ts +15 -22
  116. package/dist/core/endpoints/contractEndpoints.d.ts.map +1 -1
  117. package/dist/core/endpoints/corporationEndpoints.d.ts +19 -22
  118. package/dist/core/endpoints/corporationEndpoints.d.ts.map +1 -1
  119. package/dist/core/endpoints/corporationProjectEndpoints.d.ts +75 -23
  120. package/dist/core/endpoints/corporationProjectEndpoints.d.ts.map +1 -1
  121. package/dist/core/endpoints/createClient.d.ts +2 -2
  122. package/dist/core/endpoints/createClient.d.ts.map +1 -1
  123. package/dist/core/endpoints/dogmaEndpoints.d.ts +2 -2
  124. package/dist/core/endpoints/factionEndpoints.d.ts +36 -36
  125. package/dist/core/endpoints/factionEndpoints.d.ts.map +1 -1
  126. package/dist/core/endpoints/fleetEndpoints.d.ts +1 -1
  127. package/dist/core/endpoints/freelanceJobsEndpoints.d.ts +131 -122
  128. package/dist/core/endpoints/freelanceJobsEndpoints.d.ts.map +1 -1
  129. package/dist/core/endpoints/industryEndpoints.d.ts +6 -6
  130. package/dist/core/endpoints/industryEndpoints.d.ts.map +1 -1
  131. package/dist/core/endpoints/mailEndpoints.d.ts +3 -5
  132. package/dist/core/endpoints/mailEndpoints.d.ts.map +1 -1
  133. package/dist/core/endpoints/mercenaryEndpoints.d.ts +1 -1
  134. package/dist/core/endpoints/metaEndpoints.d.ts +5 -1
  135. package/dist/core/endpoints/metaEndpoints.d.ts.map +1 -1
  136. package/dist/core/endpoints/piEndpoints.d.ts +2 -2
  137. package/dist/core/endpoints/statusEndpoints.d.ts +1 -1
  138. package/dist/core/endpoints/universeEndpoints.d.ts +22 -22
  139. package/dist/core/endpoints/walletEndpoints.d.ts +2 -3
  140. package/dist/core/endpoints/walletEndpoints.d.ts.map +1 -1
  141. package/dist/core/logger/DefaultLogger.d.ts +31 -0
  142. package/dist/core/logger/DefaultLogger.d.ts.map +1 -0
  143. package/dist/core/logger/ILogger.d.ts +14 -4
  144. package/dist/core/logger/ILogger.d.ts.map +1 -1
  145. package/dist/core/logger/NoopLogger.d.ts +7 -0
  146. package/dist/core/logger/NoopLogger.d.ts.map +1 -0
  147. package/dist/core/logger/clientLog.d.ts +11 -0
  148. package/dist/core/logger/clientLog.d.ts.map +1 -0
  149. package/dist/core/logger/logger.d.ts +11 -3
  150. package/dist/core/logger/logger.d.ts.map +1 -1
  151. package/dist/core/logger/loggerUtil.d.ts +8 -6
  152. package/dist/core/logger/loggerUtil.d.ts.map +1 -1
  153. package/dist/core/logger/resolveLogger.d.ts +10 -0
  154. package/dist/core/logger/resolveLogger.d.ts.map +1 -0
  155. package/dist/core/pagination/AsyncPaginationIterator.d.ts +1 -0
  156. package/dist/core/pagination/AsyncPaginationIterator.d.ts.map +1 -1
  157. package/dist/core/pagination/CursorPaginationHandler.d.ts.map +1 -1
  158. package/dist/core/pagination/PaginationHandler.d.ts.map +1 -1
  159. package/dist/core/rateLimiter/RateLimiter.d.ts +6 -0
  160. package/dist/core/rateLimiter/RateLimiter.d.ts.map +1 -1
  161. package/dist/core/requestPipeline/cachePolicy.d.ts +14 -0
  162. package/dist/core/requestPipeline/cachePolicy.d.ts.map +1 -1
  163. package/dist/core/requestPipeline/fetchExecution.d.ts +1 -1
  164. package/dist/core/requestPipeline/fetchExecution.d.ts.map +1 -1
  165. package/dist/core/requestPipeline/headers.d.ts.map +1 -1
  166. package/dist/core/requestPipeline/index.d.ts +2 -2
  167. package/dist/core/requestPipeline/index.d.ts.map +1 -1
  168. package/dist/core/requestPipeline/paginationOrchestration.d.ts.map +1 -1
  169. package/dist/core/requestPipeline/statusHandling.d.ts +8 -2
  170. package/dist/core/requestPipeline/statusHandling.d.ts.map +1 -1
  171. package/dist/core/util/concurrency.d.ts +29 -0
  172. package/dist/core/util/concurrency.d.ts.map +1 -0
  173. package/dist/errors.d.ts +1 -0
  174. package/dist/errors.d.ts.map +1 -1
  175. package/dist/errors.js +53 -195
  176. package/dist/errors.js.map +1 -1
  177. package/dist/errors.mjs +37 -129
  178. package/dist/errors.mjs.map +1 -1
  179. package/dist/index.d.ts +10 -5
  180. package/dist/index.d.ts.map +1 -1
  181. package/dist/index.js +2597 -3480
  182. package/dist/index.js.map +1 -1
  183. package/dist/index.mjs +2250 -3024
  184. package/dist/index.mjs.map +1 -1
  185. package/dist/schemas/calendar.d.ts +9 -7
  186. package/dist/schemas/calendar.d.ts.map +1 -1
  187. package/dist/schemas/character.d.ts +2 -2
  188. package/dist/schemas/clones.d.ts +2 -2
  189. package/dist/schemas/common.d.ts +11 -1
  190. package/dist/schemas/common.d.ts.map +1 -1
  191. package/dist/schemas/contracts.d.ts +69 -6
  192. package/dist/schemas/contracts.d.ts.map +1 -1
  193. package/dist/schemas/corporation-projects.d.ts +96 -9
  194. package/dist/schemas/corporation-projects.d.ts.map +1 -1
  195. package/dist/schemas/corporation.d.ts +34 -22
  196. package/dist/schemas/corporation.d.ts.map +1 -1
  197. package/dist/schemas/dogma.d.ts +2 -2
  198. package/dist/schemas/faction-warfare.d.ts +125 -12
  199. package/dist/schemas/faction-warfare.d.ts.map +1 -1
  200. package/dist/schemas/fleet.d.ts +1 -1
  201. package/dist/schemas/freelance-jobs.d.ts +52 -22
  202. package/dist/schemas/freelance-jobs.d.ts.map +1 -1
  203. package/dist/schemas/index.js +420 -2474
  204. package/dist/schemas/index.js.map +1 -1
  205. package/dist/schemas/index.mjs +225 -2060
  206. package/dist/schemas/index.mjs.map +1 -1
  207. package/dist/schemas/industry.d.ts +33 -0
  208. package/dist/schemas/industry.d.ts.map +1 -1
  209. package/dist/schemas/mail.d.ts +24 -5
  210. package/dist/schemas/mail.d.ts.map +1 -1
  211. package/dist/schemas/mercenary.d.ts +1 -1
  212. package/dist/schemas/meta.d.ts +12 -1
  213. package/dist/schemas/meta.d.ts.map +1 -1
  214. package/dist/schemas/pi.d.ts +2 -2
  215. package/dist/schemas/status.d.ts +1 -1
  216. package/dist/schemas/universe.d.ts +22 -22
  217. package/dist/schemas/universe.d.ts.map +1 -1
  218. package/dist/schemas/wallet.d.ts +16 -0
  219. package/dist/schemas/wallet.d.ts.map +1 -1
  220. package/dist/sde/SdeDataProvider.d.ts.map +1 -1
  221. package/dist/sde/index.js +95 -1162
  222. package/dist/sde/index.js.map +1 -1
  223. package/dist/sde/index.mjs +73 -1091
  224. package/dist/sde/index.mjs.map +1 -1
  225. package/dist/sde/ingestion/SdeExtractor.d.ts.map +1 -1
  226. package/dist/sde/ingestion/metadata.d.ts +13 -0
  227. package/dist/sde/ingestion/metadata.d.ts.map +1 -0
  228. package/dist/sde/memory.js +21 -1095
  229. package/dist/sde/memory.js.map +1 -1
  230. package/dist/sde/memory.mjs +13 -1051
  231. package/dist/sde/memory.mjs.map +1 -1
  232. package/dist/sde/optionalPeers.d.ts +25 -0
  233. package/dist/sde/optionalPeers.d.ts.map +1 -0
  234. package/dist/testing/TestDataFactory.d.ts +28 -1
  235. package/dist/testing/TestDataFactory.d.ts.map +1 -1
  236. package/dist/testing/index.js +116 -125
  237. package/dist/testing/index.js.map +1 -1
  238. package/dist/testing/index.mjs +110 -82
  239. package/dist/testing/index.mjs.map +1 -1
  240. package/dist/types/contracts.d.ts +4 -1
  241. package/dist/types/contracts.d.ts.map +1 -1
  242. package/dist/types/corporation-projects.d.ts +5 -1
  243. package/dist/types/corporation-projects.d.ts.map +1 -1
  244. package/dist/types/faction-warfare.d.ts +9 -1
  245. package/dist/types/faction-warfare.d.ts.map +1 -1
  246. package/dist/types/freelance-jobs.d.ts +2 -1
  247. package/dist/types/freelance-jobs.d.ts.map +1 -1
  248. package/dist/types/generated/esi-spec.generated.d.ts +12 -12
  249. package/dist/types/generated/esi-spec.generated.d.ts.map +1 -1
  250. package/dist/types/generated/spec-alignment.check.d.ts +2 -2
  251. package/dist/types/industry.d.ts +2 -1
  252. package/dist/types/industry.d.ts.map +1 -1
  253. package/dist/types/mail.d.ts +2 -1
  254. package/dist/types/mail.d.ts.map +1 -1
  255. package/dist/types/meta.d.ts +2 -1
  256. package/dist/types/meta.d.ts.map +1 -1
  257. package/dist/types/wallet.d.ts +2 -1
  258. package/dist/types/wallet.d.ts.map +1 -1
  259. package/package.json +326 -289
package/README.md CHANGED
@@ -1,1030 +1,974 @@
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
- [![Coverage](https://img.shields.io/badge/coverage-90%25%2B-brightgreen)](https://github.com/lgriffin/ESI.ts)
8
- [![npm downloads](https://img.shields.io/npm/dm/%40lgriffin/esi.ts)](https://www.npmjs.com/package/@lgriffin/esi.ts)
9
- [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/lgriffin/ESI.ts/badge)](https://scorecard.dev/viewer/?uri=github.com/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
- **v9.5.2** — Supply chain security hardening: all GitHub Actions pinned by SHA, npm publish with SLSA provenance attestations, least-privilege workflow permissions, script injection prevention, and ETag cache cross-tenant isolation.
14
-
15
- **v9.5.0** — Adds 12 new ESI endpoints: CosmeticsClient (SKINR licenses, components, design lookup), ParagonHubClient (marketplace listings with cursor pagination), plus detail endpoints for Mercenary Dens, Tactical Operations, Skyhooks, and Sovereignty Hubs.
16
-
17
- **235 endpoint definitions — 206 from the public ESI OpenAPI spec, plus 29 for newer EVE features (Equinox sovereignty, orbital skyhooks, mercenary dens, access lists, freelance jobs, military campaigns, corporation projects, SKINR cosmetics, Paragon Hub marketplace). All exercisable endpoints validated against live Tranquility.**
18
-
19
- ## Why ESI.ts vs. OpenAPI-Generated Clients?
20
-
21
- 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.
22
-
23
- ### What generators give you
24
-
25
- - TypeScript interfaces from the OpenAPI spec
26
- - Basic request/response typing
27
- - A thin HTTP wrapper
28
-
29
- ### What ESI.ts gives you on top of that
30
-
31
- | Capability | openapi-typescript | ESI.ts |
32
- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
33
- | **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 200 GET endpoints have schemas. Schema mismatches throw `EsiValidationError` immediately. |
34
- | **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. |
35
- | **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. |
36
- | **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. |
37
- | **Retry & resilience** | None. | Exponential backoff with jitter, circuit breaker (closed/open/half-open), automatic 401 token refresh with concurrent coalescing. |
38
- | **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. |
39
- | **Batch operations** | None. | `batch()` with bounded concurrency for GET fan-out, `batchPost()` with auto-chunking for large POST payloads. |
40
- | **Domain knowledge** | None — generic HTTP client. | 39 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). |
41
- | **Streaming pagination** | None. | 21 domain clients with 73+ `stream*` methods via `AsyncGenerator` — process large datasets page-by-page without loading everything into memory. |
42
- | **Testing** | Whatever you write. | 167 test suites, 4,730 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). 52 runnable example scripts. |
43
-
44
- ### The real problem with generated clients
45
-
46
- The ESI OpenAPI spec is not a perfect source of truth. During live endpoint validation against the OpenAPI 3.1 spec, we discovered:
47
-
48
- - `addContacts`, `editContacts`, and 4 UI endpoints document parameters as request body when ESI actually expects query parameters
49
- - `deleteCharacterContacts` expects comma-separated contact IDs as a query param, not a JSON body
50
- - Fleet wing/squad names have a 10-character limit not documented in the spec
51
- - The `updateMailMetadata` endpoint uses the field name `read`, not `is_read`
52
-
53
- A generated client faithfully reproduces these spec bugs. ESI.ts fixes them.
54
-
55
- ## Installation
56
-
57
- ```bash
58
- npm install @lgriffin/esi.ts
59
- ```
60
-
61
- ### Building from Source
62
-
63
- ```bash
64
- git clone https://github.com/lgriffin/ESI.ts.git
65
- cd ESI.ts
66
- npm install # installs dependencies and compiles (via the prepare script)
67
- ```
68
-
69
- If you've already installed and just need to recompile:
70
-
71
- ```bash
72
- npm run build
73
- ```
74
-
75
- Verify everything works:
76
-
77
- ```bash
78
- npm run example:status # quick smoke test — checks ESI is reachable
79
- npm test # run the full test suite (167 suites, 4,730 tests)
80
- ```
81
-
82
- ## Sub-path Exports
83
-
84
- ESI.ts provides sub-path exports for targeted imports, reducing bundle size when you only need specific parts of the library:
85
-
86
- ```typescript
87
- // Zod schemas for runtime validation
88
- import { MarketOrderSchema } from '@lgriffin/esi.ts/schemas';
89
-
90
- // Error classes and type guards
91
- import { EsiError, isCircuitOpen } from '@lgriffin/esi.ts/errors';
92
-
93
- // Test utilities
94
- import { TestDataFactory } from '@lgriffin/esi.ts/testing';
95
- ```
96
-
97
- ## Static Data Export (SDE) Module
98
-
99
- ESI.ts includes a standalone module for querying CCP's EVE Online Static Data Export — 102 YAML files loaded into in-memory Maps with 109 typed interfaces, Zod validation, and ~97 query methods. No database, no external services.
100
-
101
- ```typescript
102
- import { SdeDataProvider } from '@lgriffin/esi.ts/sde';
103
-
104
- const sde = SdeDataProvider.fromDirectory('./sde-data');
105
-
106
- const tritanium = sde.getType(34);
107
- console.log(tritanium?.name); // "Tritanium"
108
-
109
- const jita = sde.getSolarSystem(30000142);
110
- const minerals = sde.getTypesByGroup(18);
111
- const caldari = sde.getFaction(500001);
112
-
113
- sde.close();
114
- ```
115
-
116
- Download SDE data with: `npx ts-node scripts/sde-ingest.ts --output sde-data`
117
-
118
- | Document | Description |
119
- | -------------------------------------------------- | --------------------------------------------------------------------------------------------- |
120
- | [SDE README](src/sde/README.md) | Module overview, quick start, full API reference (~97 methods), entity coverage table |
121
- | [Architecture](src/sde/docs/ARCHITECTURE.md) | C4 diagrams (context, container, component), data flow sequence, ER diagram, design decisions |
122
- | [Usage Guide](src/sde/docs/USAGE.md) | Provider patterns, query examples, error handling |
123
- | [Developer Guide](src/sde/docs/DEVELOPER_GUIDE.md) | Project structure, new entity checklist, field normalization, testing patterns |
124
- | [API Contracts](src/sde/docs/API_CONTRACTS.md) | Complete method reference for all IStaticDataProvider methods |
125
-
126
- ## Quick Start
127
-
128
- ```typescript
129
- import { EsiClient } from '@lgriffin/esi.ts';
130
-
131
- const client = new EsiClient();
132
-
133
- // Public data — no auth required
134
- const alliances = await client.alliance.getAlliances();
135
- const character = await client.characters.getCharacterPublicInfo(1689391488);
136
- const system = await client.universe.getSystemById(30000142);
137
- const prices = await client.market.getMarketPrices();
138
-
139
- // Authenticated data — token read from ESI_ACCESS_TOKEN env var
140
- const authedClient = new EsiClient();
141
- const assets = await authedClient.assets.getCharacterAssets(characterId);
142
- const wallet = await authedClient.wallet.getCharacterWallet(characterId);
143
-
144
- // Clean up when done
145
- await client.shutdown();
146
- ```
147
-
148
- ## Configuration
149
-
150
- ```typescript
151
- const client = new EsiClient({
152
- clientId: 'my-app', // User-Agent identifier (default: 'esi-client')
153
- accessToken: 'your-token', // EVE SSO token for authenticated endpoints
154
- baseUrl: 'https://esi.evetech.net', // ESI base URL (default)
155
- onTokenRefresh: async () => newToken, // Auto-refresh on 401 (optional)
156
- language: 'en', // Accept-Language header: en, de, fr, ja, ru, zh, ko, es (default: none)
157
- timeout: 30000, // Request timeout in ms (default: 30000)
158
- retryConfig: {
159
- maxRetries: 3, // Max retry attempts for transient errors (default: 3)
160
- baseDelayMs: 1000, // Initial backoff delay (default: 1000)
161
- maxDelayMs: 30000, // Maximum backoff delay (default: 30000)
162
- retryMutations: false, // Retry POST/PUT/DELETE (default: false, GET only)
163
- },
164
- enableETagCache: true, // ETag caching (default: true)
165
- etagCacheConfig: {
166
- maxEntries: 1000, // Max cached responses (default: 1000)
167
- defaultTtl: 300000, // Fallback TTL in ms (default: 5 min)
168
- cleanupInterval: 60000, // Expired entry cleanup interval (default: 1 min)
169
- },
170
- validateResponse: true, // Runtime Zod validation of ESI responses (default: true)
171
- validateRequest: false, // Opt-in request body Zod validation for POST/PUT/DELETE (default: false)
172
- retryStrategy: customRetryStrategy, // Injectable IRetryStrategy (default: built-in exponential backoff)
173
- circuitBreakerConfig: {
174
- keyStrategy: 'resolved', // CB keying: 'resolved' (per-URL) or 'template' (per-route) (default: 'resolved')
175
- cleanupIntervalMs: 300000, // Automatic stale circuit cleanup interval (default: 5 min)
176
- },
177
- });
178
- ```
179
-
180
- Retry is enabled by default (`maxRetries: 3`). 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. Set `maxRetries: 0` to disable retry.
181
-
182
- The access token can be updated at runtime:
183
-
184
- ```typescript
185
- client.setAccessToken('new-token');
186
- ```
187
-
188
- ## Authentication
189
-
190
- Many ESI endpoints require an EVE SSO access token. There are three ways to provide one:
191
-
192
- ### 1. Environment variable (recommended)
193
-
194
- Set `ESI_ACCESS_TOKEN` in your environment or a `.env` file. The client reads it automatically — no token in source code.
195
-
196
- ```bash
197
- # Copy the example and fill in your token
198
- cp .env.example .env
199
- ```
200
-
201
- ```env
202
- ESI_ACCESS_TOKEN=your-eve-sso-access-token
203
- ESI_CLIENT_ID=my-app-name
204
- ```
205
-
206
- If you use a `.env` loader like [dotenv](https://www.npmjs.com/package/dotenv), load it before creating the client:
207
-
208
- ```typescript
209
- import 'dotenv/config';
210
- import { EsiClient } from '@lgriffin/esi.ts';
211
-
212
- const client = new EsiClient();
213
- // Token is picked up from process.env.ESI_ACCESS_TOKEN
214
- ```
215
-
216
- ### 2. Constructor parameter
217
-
218
- Pass the token directly (useful for apps that manage tokens themselves):
219
-
220
- ```typescript
221
- const client = new EsiClient({ accessToken: token });
222
- ```
223
-
224
- ### 3. Runtime update
225
-
226
- Set or refresh the token after construction:
227
-
228
- ```typescript
229
- client.setAccessToken(newToken);
230
- ```
231
-
232
- ### Getting an EVE SSO token
233
-
234
- 1. Register an application at [EVE Developers](https://developers.eveonline.com/)
235
- 2. Set a callback URL and select the ESI scopes your app needs
236
- 3. Implement the [OAuth2 flow](https://docs.esi.evetech.net/docs/sso/) to obtain an access token
237
- 4. Access tokens expire — use the refresh token to get new ones
238
-
239
- ### Automatic Token Refresh
240
-
241
- 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:
242
-
243
- ```typescript
244
- const client = new EsiClient({
245
- accessToken: initialToken,
246
- onTokenRefresh: async () => {
247
- const response = await fetch('https://login.eveonline.com/v2/oauth/token', {
248
- method: 'POST',
249
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
250
- body: new URLSearchParams({
251
- grant_type: 'refresh_token',
252
- refresh_token: myRefreshToken,
253
- client_id: myClientId,
254
- }),
255
- });
256
- const { access_token } = await response.json();
257
- return access_token;
258
- },
259
- });
260
-
261
- // Requests now auto-refresh on 401 — no manual token management needed
262
- const location = await client.location.getCharacterLocation(characterId);
263
- ```
264
-
265
- The token provider can also be set or changed at runtime:
266
-
267
- ```typescript
268
- client.setTokenProvider(myRefreshFunction);
269
- client.setTokenProvider(undefined); // disable auto-refresh
270
- ```
271
-
272
- Key behaviors:
273
-
274
- - Only retries **once** per request — if the refreshed token also gets a 401, the error is thrown
275
- - **Concurrent coalescing** — if multiple requests hit 401 simultaneously, only one refresh call is made
276
- - If the refresh callback throws (e.g., refresh token revoked), a `TOKEN_REFRESH_FAILED` error is raised
277
- - Without a token provider, 401 errors throw immediately as before
278
-
279
- ### Environment variables reference
280
-
281
- | Variable | Description | Default |
282
- | ------------------ | -------------------------------------------- | ------------------------- |
283
- | `ESI_ACCESS_TOKEN` | EVE SSO access token | none |
284
- | `ESI_CLIENT_ID` | User-Agent identifier | `esi-client` |
285
- | `ESI_BASE_URL` | ESI API base URL | `https://esi.evetech.net` |
286
- | `ESI_LOG_LEVEL` | Log level (`error`, `warn`, `info`, `debug`) | `warn` |
287
-
288
- ## Available APIs
289
-
290
- All clients are accessed as properties on the `EsiClient` instance. Authenticated endpoints require an access token.
291
-
292
- | Client | Property | Auth | Examples |
293
- | ------------------ | ---------------------------- | ---- | ------------------------------------------------------------------------------------- |
294
- | Alliance | `client.alliance` | Some | `getAlliances()`, `getAllianceById(id)` |
295
- | Assets | `client.assets` | Yes | `getCharacterAssets(id)` |
296
- | Calendar | `client.calendar` | Yes | `getCalendarEvents(id)` |
297
- | Characters | `client.characters` | Some | `getCharacterPublicInfo(id)`, `getCharacterPortrait(id)` |
298
- | Clones | `client.clones` | Yes | `getCharacterClones(id)` |
299
- | Contacts | `client.contacts` | Yes | `getCharacterContacts(id)`, `postCharacterContacts(id, standing, contactIds)` |
300
- | Contracts | `client.contracts` | Yes | `getCharacterContracts(id)` |
301
- | Corp Projects | `client.corporationProjects` | Yes | `getCorporationProjects(corpId)`, `getCorporationProject(corpId, projectId)` |
302
- | Corporations | `client.corporations` | Some | `getCorporationInfo(id)`, `getCorporationMembers(id)` |
303
- | Dogma | `client.dogma` | No | `getDogmaAttributes()`, `getDynamicItemInfo(typeId, itemId)` |
304
- | Factions | `client.factions` | Some | `getFactionWarStats()` |
305
- | Fittings | `client.fittings` | Yes | `getFittings(id)`, `createFitting(id, body)` |
306
- | Fleets | `client.fleets` | Yes | `getFleetInformation(id)`, `getFleetMembers(id)` |
307
- | Incursions | `client.incursions` | No | `getIncursions()` |
308
- | Industry | `client.industry` | Some | `getCharacterIndustryJobs(id)` |
309
- | Insurance | `client.insurance` | No | `getInsurancePrices()` |
310
- | Killmails | `client.killmails` | Some | `getKillmail(id, hash)` |
311
- | Location | `client.location` | Yes | `getCharacterLocation(id)` |
312
- | Loyalty | `client.loyalty` | Yes | `getCharacterLoyaltyPoints(id)` |
313
- | Mail | `client.mail` | Yes | `getCharacterMail(id)`, `sendMail(id, body)` |
314
- | Market | `client.market` | Some | `getMarketPrices()`, `getMarketOrders(regionId)` |
315
- | Military Campaigns | `client.militaryCampaigns` | Some | `getMilitaryCampaigns()`, `getMilitaryCampaignById(id)` |
316
- | PI | `client.pi` | Yes | `getCharacterPlanets(id)` |
317
- | Route | `client.route` | No | `getRoute(origin, destination)` |
318
- | Search | `client.search` | Some | `search(characterId, query)` |
319
- | Skills | `client.skills` | Yes | `getCharacterSkills(id)` |
320
- | Sovereignty | `client.sovereignty` | No | `getSovereigntySystems()`, `getSovereigntyMap()` |
321
- | Skyhooks | `client.skyhooks` | Some | `getSovereigntyHubs(corpId)`, `getSkyhookDetail(corpId, id)`, `getRaidableSkyhooks()` |
322
- | Mercenary | `client.mercenary` | Yes | `getMercenaryDens(charId)`, `getMercenaryDenDetail(charId, denId)` |
323
- | Cosmetics | `client.cosmetics` | Some | `getSkinr(id)`, `getCharacterSkinr(charId)`, `getCharacterSkinrComponents(charId)` |
324
- | Paragon Hub | `client.paragonHub` | Some | `getPublicListings()`, `getCharacterListings(charId)`, `getAllianceListings(id)` |
325
- | Access Lists | `client.accessLists` | Yes | `getAccessList(id)` |
326
- | Status | `client.status` | No | `getStatus()` |
327
- | UI | `client.ui` | Yes | `setAutopilotWaypoint(destId, addToBeginning, clear)`, `openNewMailWindow(body)` |
328
- | Universe | `client.universe` | Some | `getSystemById(id)`, `getTypeById(id)` |
329
- | Wallet | `client.wallet` | Yes | `getCharacterWallet(id)` |
330
- | Wars | `client.wars` | No | `getWars()`, `getWarById(id)` |
331
- | Freelance Jobs | `client.freelanceJobs` | Some | `getFreelanceJobs()`, `getFreelanceJobById(id)` |
332
- | Meta | `client.meta` | No | `getOpenApiJson()`, `getOpenApiYaml()` |
333
-
334
- ## Runtime Response Validation
335
-
336
- 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.
337
-
338
- 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.
339
-
340
- ```typescript
341
- import {
342
- EsiClient,
343
- EsiValidationError,
344
- isValidationError,
345
- schemas,
346
- } from '@lgriffin/esi.ts';
347
-
348
- const client = new EsiClient();
349
-
350
- // Validation happens automatically on every request
351
- const character = await client.characters.getCharacterPublicInfo(12345);
352
-
353
- // Disable validation globally if needed
354
- const rawClient = new EsiClient({ validateResponse: false });
355
-
356
- // Use schemas directly for your own validation
357
- const result = schemas.CharacterInfoSchema.safeParse(someData);
358
- if (result.success) {
359
- console.log(result.data.name);
360
- }
361
- ```
362
-
363
- ### Request Body Validation
364
-
365
- For POST/PUT/DELETE endpoints, opt-in request body validation ensures outgoing payloads match the endpoint's `requestSchema` before the request is sent:
366
-
367
- ```typescript
368
- // Opt-in request body validation for POST/PUT/DELETE
369
- const client = new EsiClient({ validateRequest: true });
370
-
371
- // Throws EsiValidationError if the request body doesn't match the endpoint's requestSchema
372
- await client.mail.sendMail(characterId, {
373
- recipients: [{ recipient_id: 12345, recipient_type: 'character' }],
374
- subject: 'Hello',
375
- body: 'Message body',
376
- });
377
- ```
378
-
379
- See [guides/RUNTIME-VALIDATION.md](guides/RUNTIME-VALIDATION.md) for the full guide on schemas, error handling, and extending schemas.
380
-
381
- ## Caching
382
-
383
- ETag caching is enabled by default. The client automatically:
384
-
385
- 1. Stores ETag and response data on GET requests
386
- 2. Sends `If-None-Match` on subsequent requests
387
- 3. Returns cached data on `304 Not Modified`
388
- 4. Parses `Cache-Control: max-age` from ESI for per-endpoint TTL
389
- 5. Serves stale cached data when ESI returns 5xx errors
390
- 6. Invalidates related GET caches when POST/PUT/DELETE requests are made
391
-
392
- ```typescript
393
- // Cache stats
394
- const stats = client.getCacheStats();
395
- console.log(`${stats.totalEntries}/${stats.maxEntries} entries cached`);
396
-
397
- // Manual cache operations
398
- client.clearCache();
399
- client.updateCacheConfig({ maxEntries: 2000 });
400
-
401
- // Disable caching entirely
402
- const uncachedClient = new EsiClient({ enableETagCache: false });
403
- ```
404
-
405
- ### Spec-Aware Cache TTLs
406
-
407
- 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.
408
-
409
- This layers on top of ETag caching in three tiers:
410
-
411
- 1. **Spec TTL** — data can't have changed yet, return cached data immediately
412
- 2. **ETag conditional GET** — data might have changed, send `If-None-Match` to check
413
- 3. **Full request** — no cache entry, fetch fresh data
414
-
415
- ```typescript
416
- const client = new EsiClient();
417
-
418
- // First call — fetches from ESI
419
- const alliances = await client.alliance.getAlliances();
420
-
421
- // Second call within the next 3600s — returns cached data, zero HTTP calls
422
- const same = await client.alliance.getAlliances();
423
- ```
424
-
425
- ## Batch Requests
426
-
427
- Fetch data for multiple IDs with bounded concurrency using `batch()`, or chunk large POST payloads with `batchPost()`:
428
-
429
- ```typescript
430
- import { EsiClient } from '@lgriffin/esi.ts';
431
-
432
- const client = new EsiClient();
433
-
434
- // Fetch 500 type details with at most 10 concurrent requests
435
- const result = await client.batch(
436
- typeIds,
437
- (id) => client.universe.getTypeById(id),
438
- {
439
- concurrency: 10,
440
- onProgress: (done, total) => console.log(`${done}/${total}`),
441
- },
442
- );
443
-
444
- // result.results: Map<number, T> — successful responses
445
- // result.errors: Map<number, Error> — failed requests
446
- console.log(`${result.results.size} succeeded, ${result.errors.size} failed`);
447
- ```
448
-
449
- For POST endpoints that accept arrays (e.g., `postUniverseNames` with a 1000-ID limit), `batchPost` auto-chunks and concatenates:
450
-
451
- ```typescript
452
- const allNames = await client.batchPost(
453
- largeIdArray,
454
- (chunk) => client.universe.postUniverseNames(chunk),
455
- 1000, // chunk size
456
- );
457
- ```
458
-
459
- ## Streaming Pagination
460
-
461
- 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:
462
-
463
- ```typescript
464
- import { EsiClient } from '@lgriffin/esi.ts';
465
-
466
- const client = new EsiClient();
467
-
468
- // Stream all market orders in The Forge, page by page
469
- for await (const page of client.market.streamMarketOrders(10000002)) {
470
- console.log(
471
- `Page ${page.page}/${page.totalPages}: ${page.data.length} orders`,
472
- );
473
-
474
- // Process each order as it arrives
475
- for (const order of page.data) {
476
- if (order.is_buy_order && order.price > 1_000_000) {
477
- console.log(`High-value buy: ${order.type_id} @ ${order.price} ISK`);
478
- }
479
- }
480
-
481
- // Early termination — stops fetching remaining pages
482
- if (page.page >= 3) break;
483
- }
484
- ```
485
-
486
- 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.
487
-
488
- Available streaming methods (representative selection):
489
-
490
- - **MarketClient** — `streamMarketOrders`, `streamMarketTypes`, `streamCharacterOrderHistory`, `streamCorporationOrders`, `streamCorporationOrderHistory`, `streamMarketOrdersInStructure`
491
- - **CorporationsClient** — `streamCorporationMembers`, `streamCorporationStructures`, `streamCorporationBlueprints`, + 14 more
492
- - **CharacterClient** — `streamCharacterBlueprints`, `streamCharacterNotifications`, `streamCharacterStandings`, + 5 more
493
- - **ContractsClient** — `streamPublicContracts`, `streamCharacterContracts`, `streamCorporationContracts`
494
- - **WalletClient** — `streamCharacterWalletJournal`, `streamCorporationWalletJournal`, `streamCharacterWalletTransactions`
495
- - **IndustryClient** — `streamCorporationIndustryJobs`, `streamCorporationMiningObservers`, + 6 more
496
- - **ContactsClient** — `streamAllianceContacts`, `streamCharacterContacts`, `streamCorporationContacts`, + 3 more
497
- - **AssetsClient** — `streamCharacterAssets`, `streamCorporationAssets`
498
- - **KillmailsClient** — `streamCharacterRecentKillmails`, `streamCorporationRecentKillmails`
499
- - **MailClient** — `streamCharacterMail`, `streamCharacterMailLabels`
500
- - **FleetsClient** — `streamFleetMembers`, `streamFleetWings`
501
- - **CalendarClient** — `streamCalendarEvents`
502
- - **FittingsClient** — `streamCharacterFittings`
503
- - **SkillsClient** — `streamCharacterSkillQueue`
504
- - **LoyaltyClient** — `streamCorporationLoyaltyStoreOffers`
505
- - **BookmarksClient** — `streamCharacterBookmarks`, `streamCorporationBookmarks`
506
- - **ClonesClient** — `streamCharacterImplants`
507
- - **PIClient** — `streamCharacterPlanets`
508
- - **WarsClient** — `streamWars`
509
- - **FactionWarfareClient** — `streamFactionWarfareStats`
510
- - **AllianceClient** — `streamAllianceCorporations`
511
-
512
- Try it: `npm run example:streaming`
513
-
514
- ## Cursor-based Pagination
515
-
516
- 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.
517
-
518
- ```typescript
519
- import { EsiClient, fetchAllCursorPages } from '@lgriffin/esi.ts';
520
-
521
- const client = new EsiClient();
522
-
523
- // Fetch first page — returns { cursor: { before, after }, freelance_jobs: [...] }
524
- const page = await client.freelanceJobs.getFreelanceJobs();
525
- console.log(page.freelance_jobs); // job records
526
- console.log(page.cursor.after); // opaque token for next page
527
-
528
- // Fetch next page using the cursor
529
- const nextPage = await client.freelanceJobs.getFreelanceJobs(
530
- undefined,
531
- page.cursor.after,
532
- );
533
-
534
- // Auto-fetch all pages in one call
535
- const allJobs = await fetchAllCursorPages(
536
- (before, after) => client.freelanceJobs.getFreelanceJobs(before, after),
537
- (response) => response.freelance_jobs,
538
- (response) => response.cursor,
539
- );
540
-
541
- // Authenticated endpoints — character/corporation freelance jobs
542
- const authedClient = new EsiClient({ accessToken: 'your-token' });
543
- const myJobs =
544
- await authedClient.freelanceJobs.getCharacterFreelanceJobs(characterId);
545
- const corpJobs =
546
- await authedClient.freelanceJobs.getCorporationFreelanceJobs(corporationId);
547
- ```
548
-
549
- **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:
550
-
551
- ```typescript
552
- // After initial scan, save the final cursor
553
- let savedCursor = lastPage.cursor.after;
554
-
555
- // Later: check for updates (hours, days, or weeks later)
556
- const updates = await client.freelanceJobs.getFreelanceJobs(
557
- undefined,
558
- savedCursor,
559
- );
560
- if (updates.freelance_jobs.length > 0) {
561
- // Process changed records — duplicates are expected for modified records
562
- savedCursor = updates.cursor.after;
563
- }
564
- ```
565
-
566
- Key points:
567
-
568
- - Cursor tokens are **opaque strings** — never parse or validate them
569
- - An **empty result array** signals the end of the dataset (not a short page)
570
- - **Duplicates across pages** are expected when records are modified between requests
571
- - Existing offset-based routes (`getMarketOrders`, etc.) are unchanged
572
-
573
- ## Generated Types
574
-
575
- 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:
576
-
577
- ```typescript
578
- import { EsiSpec } from '@lgriffin/esi.ts';
579
-
580
- // Generated type — uses OpenAPI schema names (v7.0.0+)
581
- const order: EsiSpec.MarketsRegionIdOrdersGet = {
582
- order_id: 123,
583
- type_id: 34,
584
- price: 5.5,
585
- volume_remain: 1000,
586
- volume_total: 5000,
587
- is_buy_order: false,
588
- // ...
589
- };
590
- ```
591
-
592
- To regenerate types from the latest ESI spec:
593
-
594
- ```bash
595
- npm run generate:types # fetches OpenAPI spec, generates 161 interfaces + cache TTL map + rate limit groups + scope map
596
- npm run validate:esi # reports type drift between hand-written and generated types
597
- ```
598
-
599
- ## ESI Scopes
600
-
601
- 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:
602
-
603
- ```typescript
604
- import { esiEndpointScopes, EsiScope } from '@lgriffin/esi.ts';
605
-
606
- // Look up scopes for a specific endpoint
607
- const walletScopes = esiEndpointScopes['GET:characters/{character_id}/wallet'];
608
- // → ['esi-wallet.read_character_wallet.v1']
609
-
610
- // Check if an endpoint requires auth
611
- const isPublic = !esiEndpointScopes['GET:universe/types/{type_id}'];
612
- // → true (public endpoint, no scopes needed)
613
-
614
- // Type-safe scope values
615
- const scope: EsiScope = 'esi-assets.read_assets.v1';
616
- ```
617
-
618
- ## Error Handling
619
-
620
- API errors throw `EsiError` with `statusCode`, `message`, and `url` properties:
621
-
622
- ```typescript
623
- import {
624
- EsiError,
625
- TimeoutError,
626
- EsiValidationError,
627
- isTimeout,
628
- isRetryable,
629
- isValidationError,
630
- isCircuitOpen,
631
- } from '@lgriffin/esi.ts';
632
-
633
- try {
634
- const alliance = await client.alliance.getAllianceById(99999999);
635
- console.log('Alliance:', alliance.name);
636
- } catch (err) {
637
- if (isCircuitOpen(err)) {
638
- console.log('Circuit breaker is open — endpoint temporarily unavailable');
639
- } else if (isValidationError(err)) {
640
- console.log('Response validation failed:', err.validationError);
641
- } else if (isTimeout(err)) {
642
- console.log(`Request timed out after ${err.timeoutMs}ms`);
643
- } else if (err instanceof EsiError) {
644
- console.log(`ESI error ${err.statusCode}: ${err.message}`);
645
- console.log(`Retryable: ${err.retryable}`);
646
- }
647
- }
648
- ```
649
-
650
- - **204 No Content** — returns `undefined` (valid for DELETE/POST actions)
651
- - **304 Not Modified** — handled internally, returns cached data
652
- - **4xx/5xx** — throws `EsiError`
653
- - **5xx with cache** — returns stale cached data instead of throwing
654
- - **Timeout** — throws `TimeoutError` (extends `EsiError` with `statusCode: 0` and `timeoutMs`)
655
- - **Retryable errors** — `EsiError.retryable` returns `true` for 502, 503, 504, 420, 429, and timeouts
656
- - **Validation errors** — throws `EsiValidationError` (extends `EsiError`) when response data doesn't match the expected Zod schema
657
-
658
- ## Response Metadata
659
-
660
- Use `withMetadata()` to get response headers, cache status, rate limit info, and timing alongside the data:
661
-
662
- ```typescript
663
- const metaClient = client.alliance.withMetadata();
664
- const result = await metaClient.getAllianceById(99000001);
665
-
666
- console.log(result.data.name); // "Goonswarm Federation"
667
- console.log(result.meta.fromCache); // true if served from cache
668
- console.log(result.meta.cacheHitType); // 'spec-ttl' | 'etag-304' | 'stale-on-error'
669
- console.log(result.meta.responseTimeMs); // milliseconds
670
- console.log(result.meta.rateLimit); // { remaining, limit, used, group }
671
- console.log(result.meta.requestId); // ESI request ID for debugging
672
- ```
673
-
674
- The `meta` object includes:
675
-
676
- | Field | Type | Description |
677
- | ---------------- | ------------------------ | ------------------------------------------------- |
678
- | `headers` | `Record<string, string>` | Raw response headers |
679
- | `fromCache` | `boolean` | Whether data was served from cache |
680
- | `stale` | `boolean` | Whether cached data is stale (5xx fallback) |
681
- | `cacheHitType` | `string?` | `'spec-ttl'`, `'etag-304'`, or `'stale-on-error'` |
682
- | `rateLimit` | `RateLimitMeta?` | Rate limit status from ESI headers |
683
- | `responseTimeMs` | `number?` | Request duration in milliseconds |
684
- | `requestId` | `string?` | ESI request ID |
685
- | `warning` | `object?` | ESI deprecation warning |
686
-
687
- ## Rate Limiting
688
-
689
- 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.
690
-
691
- Rate limiting works out of the box with no configuration. For multi-character applications, enable per-user bucketing:
692
-
693
- ```typescript
694
- import { EsiClient } from '@lgriffin/esi.ts';
695
-
696
- const client = new EsiClient({
697
- rateLimiterConfig: {
698
- userKeyExtractor: (headers) => headers['authorization'] ?? 'anon',
699
- },
700
- });
701
- ```
702
-
703
- Monitor rate limit status per group:
704
-
705
- ```typescript
706
- const limiter = client.getRateLimiter();
707
-
708
- // Worst-case across all groups (backward-compatible)
709
- const status = limiter.getStatus();
710
- console.log(status.remaining, status.limit, status.group);
711
-
712
- // Specific group
713
- const marketStatus = limiter.getGroupStatus('market-order');
714
- console.log(marketStatus?.remaining); // tokens remaining in this group
715
-
716
- // All active groups
717
- const all = limiter.getAllGroupStatuses();
718
- for (const [group, info] of all) {
719
- console.log(`${group}: ${info.remaining}/${info.limit}`);
720
- }
721
-
722
- // Check if a specific group is blocked
723
- console.log(limiter.isBlocked('char-notification')); // true if 429'd
724
- ```
725
-
726
- ## Lightweight Clients
727
-
728
- 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.
729
-
730
- If you only need a subset of APIs, use `CustomEsiClient` or `EsiClientBuilder` to load only what you need:
731
-
732
- ```typescript
733
- import { EsiClientBuilder } from '@lgriffin/esi.ts';
734
-
735
- const client = new EsiClientBuilder()
736
- .addClients(['market', 'universe', 'characters'])
737
- .withClientId('my-trading-bot')
738
- .withAccessToken('your-token')
739
- .build();
740
-
741
- const prices = await client.market?.getMarketPrices();
742
- const system = await client.universe?.getSystemById(30000142);
743
- ```
744
-
745
- Or create standalone single-API clients:
746
-
747
- ```typescript
748
- import { EsiApiFactory } from '@lgriffin/esi.ts';
749
-
750
- const marketClient = EsiApiFactory.createMarketClient({
751
- clientId: 'price-checker',
752
- });
753
- const prices = await marketClient.getMarketPrices();
754
- ```
755
-
756
- ## Endpoint Coverage
757
-
758
- All 235 endpoint definitions have been validated against live Tranquility using the **OpenAPI 3.1 spec** — 206 from the public ESI spec plus 29 for newer EVE features. Full output is captured in [`openapi.output.md`](openapi.output.md).
759
-
760
- | Category | Endpoints | Method |
761
- | --------------------------- | --------- | -------------------------------------------------- |
762
- | Public GETs | 86 | 52 runnable example scripts with captured output |
763
- | Authenticated GETs | 114 | Example scripts + live testing with EVE SSO tokens |
764
- | Contacts (POST/PUT/DELETE) | 3 | Live create/edit/delete lifecycle |
765
- | Fittings (POST/DELETE) | 2 | Live create/delete lifecycle |
766
- | Mail (POST/PUT/DELETE) | 5 | Live send/label/metadata/delete lifecycle |
767
- | UI (POST) | 5 | Live testing with EVE client running |
768
- | Calendar (PUT) | 1 | Live RSVP to event |
769
- | Fleet (GET/POST/PUT/DELETE) | 14 | Live fleet with fleet commander + squad members |
770
- | Assets POST | 3 | Live asset location/name queries |
771
- | CSPA (POST) | 1 | Live charge cost calculation |
772
- | Dogma dynamic (GET) | 1 | Live mutaplasmid (Abyssal) item query |
773
- | Universe POST helpers | 3 | Live name resolution and affiliation |
774
- | Freelance Jobs (GET) | 4 | Live queries (graceful 404 for no active jobs) |
775
-
776
- ## Examples
777
-
778
- 52 runnable examples are in the `examples/` directory.
779
-
780
- ### Public Endpoints (no auth needed)
781
-
782
- ```bash
783
- npm run example:status # Server status — quickest smoke test
784
- npm run example:character # Character public info, portrait, corporation
785
- npm run example:universe # Solar system, constellation, region, station
786
- npm run example:market # Average prices + Tritanium price history
787
- npm run example:alliance # Alliance info + member corporations
788
- npm run example:route # Jita-to-Amarr route with system names
789
- npm run example:wars # Recent wars with aggressor/defender details
790
- npm run example:sovereignty # Nullsec sovereignty map + active campaigns
791
- npm run example:industry # Industry facilities, cost indices, insurance
792
- npm run example:incursions # Active incursions + faction warfare stats
793
- npm run example:dogma # Item type details + dogma attributes
794
- npm run example:contracts # Public region contracts + auction bids/items
795
- npm run example:rate-limiting # Rate limiter & pagination demonstration
796
- npm run example:cursor-pagination # Freelance Jobs with cursor pagination
797
- npm run example:streaming # Streaming pagination for large datasets
798
- npm run example:token-refresh # Automatic token refresh on 401
799
- npm run example:universe-encyclopedia # Ancestries, bloodlines, races, celestials
800
- npm run example:dogma-meta-sov # Dogma effects, sovereignty, meta endpoint
801
- npm run example:faction-details # Faction warfare leaderboards and stats
802
- ```
803
-
804
- ### Authenticated Endpoints (require ESI_ACCESS_TOKEN)
805
-
806
- ```bash
807
- npm run example # Full character profile assembly
808
- npm run example:wallet # Wallet balance, journal, transactions
809
- npm run example:skills # Trained skills, queue, attributes
810
- npm run example:assets # Asset inventory with bulk name lookup
811
- npm run example:killmails # Recent killmails + full details
812
- npm run example:fleet # Fleet info, members, wing/squad structure
813
- npm run example:mail # Inbox headers, labels, mailing lists
814
- npm run example:location # Current system, online status, ship
815
- npm run example:fittings # Saved fittings + clone state + implants
816
- npm run example:contacts # Contact list with standings + labels
817
- npm run example:character-details # Blueprints, roles, standings, medals
818
- npm run example:corporation-details # Corp members, divisions, structures
819
- npm run example:calendar-search # Calendar events + character search
820
- npm run example:loyalty-pi # Loyalty points + planetary interaction
821
- npm run example:industry-mining # Industry jobs + mining ledger
822
- npm run example:market-orders # Character/corp market orders
823
- npm run example:corp-contracts-wallet # Corp contracts, contacts, wallets
824
- ```
825
-
826
- ### Write Operations (require specific scopes + caution)
827
-
828
- ```bash
829
- npm run example:write-ops # Contacts, fittings, mail, UI lifecycle tests
830
- npm run example:universe-posts # Name resolution + character affiliation (public)
831
- npm run example:freelance-jobs # Freelance job queries
832
- ```
833
-
834
- ### Parallel Requests
835
-
836
- ```typescript
837
- const [character, portrait, corp] = await Promise.all([
838
- client.characters.getCharacterPublicInfo(characterId),
839
- client.characters.getCharacterPortrait(characterId),
840
- client.corporations.getCorporationInfo(corporationId),
841
- ]);
842
-
843
- console.log(`${character.name} [${corp.ticker}]`);
844
- ```
845
-
846
- ### Market Analysis
847
-
848
- ```typescript
849
- const [orders, history] = await Promise.all([
850
- client.market.getMarketOrders(regionId),
851
- client.market.getMarketHistory(regionId, typeId),
852
- ]);
853
-
854
- const buyOrders = orders.filter((o) => o.is_buy_order);
855
- const sellOrders = orders.filter((o) => !o.is_buy_order);
856
-
857
- console.log(`Best buy: ${Math.max(...buyOrders.map((o) => o.price))}`);
858
- console.log(`Best sell: ${Math.min(...sellOrders.map((o) => o.price))}`);
859
- ```
860
-
861
- ## Resource Management
862
-
863
- Always call `shutdown()` when you're done to clean up cache timers:
864
-
865
- ```typescript
866
- const client = new EsiClient();
867
- try {
868
- const status = await client.status.getStatus();
869
- console.log(status.server_version);
870
- } finally {
871
- await client.shutdown();
872
- }
873
- ```
874
-
875
- ## Testing
876
-
877
- ESI.ts has a comprehensive multi-tier testing strategy with 139 suites and 4,104 tests:
878
-
879
- | Tier | Tests | Purpose |
880
- | -------------------------- | ---------------- | ------------------------------------------------------------------ |
881
- | **TDD unit tests** | 100 files | Every client method, endpoint path, query param, and body format |
882
- | **BDD scenario tests** | 41 feature files | Behavioral specifications in Gherkin (Given/When/Then) |
883
- | **Mocked integration** | Full suite | Cross-layer request flow with jest-fetch-mock |
884
- | **Live smoke tests** | 46 examples | Every endpoint against live Tranquility |
885
- | **ESI spec contract** | 15 tests | Endpoint definitions validated against live OpenAPI spec |
886
- | **Deep contract tests** | 8 categories | Path params, query params, body, auth, schemas, pagination vs spec |
887
- | **Property-based fuzzing** | 601 tests | fast-check fuzzing of validation, URL construction, Zod schemas |
888
- | **Mutation testing** | Stryker | Validates test suite kills code mutants |
889
- | **Type-level tests** | tsd | Consumer API type correctness via tsd |
890
- | **Gated auth tests** | 33 tests | Authenticated endpoints with real tokens |
891
- | **Construction parity** | Per-surface | Verifies all client surfaces get identical middleware defaults |
892
- | **Spec-alignment** | Type assertions | Ensures hand-written types align with generated OpenAPI types |
893
-
894
- ```bash
895
- npm test # Unit + BDD tests (167 suites, 4,730 tests)
896
- npm run coverage # Tests with coverage report (thresholds enforced)
897
- npm run bdd # BDD scenario tests only
898
- npm run contract # Contract tests (skipped without ESI_LIVE_TESTS=true)
899
- npm run fuzz # Property-based fuzz tests (601 tests)
900
- npm run mutation # Mutation testing (Stryker)
901
- npm run benchmark # Performance benchmark tests
902
- npm run test:types # tsd consumer type tests
903
- ```
904
-
905
- Coverage: statements 98.47%, branches 90.10%, functions 97.54%, lines 98.59%. Thresholds enforced in CI: branches 80%, functions 75%, lines 90%, statements 90%.
906
-
907
- See [guides/TESTING.md](guides/TESTING.md) for the full testing guide, and [guides/ARCHITECTURE.md](guides/ARCHITECTURE.md) for architecture diagrams.
908
-
909
- ## Development
910
-
911
- ### Prerequisites
912
-
913
- - Node.js 18+
914
- - npm
915
-
916
- ### Code Quality Tools
917
-
918
- The project uses a comprehensive suite of static analysis and code quality tools:
919
-
920
- | Tool | Purpose | Command |
921
- | ------------------------------------------------------------------------------------ | ------------------------------------------------------- | ------------------------------ |
922
- | [ESLint](https://eslint.org/) | Linting with TypeScript, security, and code smell rules | `npm run lint` |
923
- | [Prettier](https://prettier.io/) | Code formatting | `npm run format:check` |
924
- | [knip](https://knip.dev/) | Dead code and unused export detection | `npm run knip` |
925
- | [eslint-plugin-security](https://github.com/eslint-community/eslint-plugin-security) | Security anti-pattern detection | Integrated into `npm run lint` |
926
- | [eslint-plugin-sonarjs](https://github.com/SonarSource/eslint-plugin-sonarjs) | Cognitive complexity and code smell detection | Integrated into `npm run lint` |
927
- | [husky](https://typicode.github.io/husky/) | Git pre-commit hooks | Automatic on commit |
928
- | [lint-staged](https://github.com/lint-staged/lint-staged) | Run linters on staged files only | Automatic on commit |
929
- | [Redocly CLI](https://redocly.com/docs/cli/) | OpenAPI spec validation and linting | `npm run validate:spec` |
930
-
931
- ### Available Scripts
932
-
933
- ```bash
934
- # Development
935
- npm run build # Compile TypeScript
936
- npm run lint # Run ESLint
937
- npm run lint:fix # Run ESLint with auto-fix
938
- npm run format # Format code with Prettier
939
- npm run format:check # Check formatting without modifying
940
-
941
- # Testing
942
- npm test # Unit tests (167 suites, 4,730 tests)
943
- npm run test:all # Unit + BDD + integration + fuzz + type tests
944
- npm run coverage # Tests with coverage report (thresholds enforced)
945
- npm run bdd # BDD scenario tests
946
- npm run contract:live # Deep contract tests against live ESI spec
947
- npm run fuzz # Property-based fuzz tests (fast-check)
948
- npm run mutation # Mutation testing (Stryker)
949
- npm run benchmark # Performance benchmark tests
950
- npm run test:types # Consumer type tests (tsd)
951
- npm run mock:esi # Start Prism mock ESI server on port 4010
952
-
953
- # Static Analysis
954
- npm run knip # Detect dead code and unused exports
955
- npm run validate:esi # Validate endpoints against live ESI OpenAPI spec
956
- npm run validate:spec # Lint ESI OpenAPI spec with Redocly (structural + best practices)
957
- npm run validate:auth-scopes # Auth/scope cross-validation
958
- npm run schema:drift # Schema drift detection (hand-written vs OpenAPI spec)
959
- npm run validate # Run all checks: lint, format, build, coverage, knip
960
- npm run generate:types # Regenerate TypeScript interfaces from ESI OpenAPI spec
961
- npm run generate:endpoints # Regenerate endpoint definitions from ESI OpenAPI spec
962
- npm run generate:all # Run all generators (types + endpoints + OKF)
963
- npm run generate:okf # Generate OKF knowledge bundle from ESI OpenAPI spec
964
-
965
- # Documentation
966
- npm run docs # Generate TypeDoc API documentation
967
- npm run docs:serve # Serve docs locally on port 8080
968
- ```
969
-
970
- ### ESI Endpoint Validation
971
-
972
- To verify that the codebase endpoint definitions match the live ESI OpenAPI spec:
973
-
974
- ```bash
975
- npm run validate:esi
976
- ```
977
-
978
- This fetches the ESI OpenAPI spec and reports:
979
-
980
- - Endpoints in the codebase that are no longer in the ESI spec
981
- - Endpoints in the ESI spec that the codebase doesn't cover
982
- - HTTP method mismatches between codebase and spec
983
-
984
- ### Pre-commit Hooks
985
-
986
- 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`.
987
-
988
- ### CI/CD
989
-
990
- Every pull request runs the full validation suite:
991
-
992
- - ESLint (with security and sonarjs plugins)
993
- - Prettier formatting check
994
- - TypeScript compilation
995
- - Generated types staleness check (regenerates from live ESI OpenAPI spec and verifies no diff)
996
- - Unit tests across Node.js 18, 20, and 22
997
- - BDD scenario tests
998
- - Coverage threshold enforcement (branches: 80%, functions: 75%, lines: 90%, statements: 90%)
999
- - Auth/scopes cross-validation
1000
- - Spec-alignment type assertions
1001
- - Schema drift detection
1002
- - Mutation testing (Stryker)
1003
- - Dead code detection via knip
1004
- - npm security audit
1005
-
1006
- **Supply chain security:**
1007
-
1008
- - All GitHub Actions pinned by SHA hash (not mutable tags) to prevent supply chain attacks
1009
- - Least-privilege `permissions:` on all workflows and jobs
1010
- - Script injection prevention (user-controlled inputs passed via `env:`, never interpolated in `run:`)
1011
- - npm publish with `--provenance` for SLSA attestations (verifiable build origin)
1012
- - OpenSSF Scorecard runs weekly via the `scorecard.yml` workflow
1013
-
1014
- See [.github/workflows/README.md](.github/workflows/README.md) for full workflow details.
1015
-
1016
- ## Contributing
1017
-
1018
- 1. Fork the repository
1019
- 2. Create a feature branch
1020
- 3. Write tests for your changes
1021
- 4. Run `npm run validate` to check everything passes
1022
- 5. Open a Pull Request
1023
-
1024
- ## License
1025
-
1026
- GPL-3.0-or-later - see the [LICENSE](LICENSE) file for details.
1027
-
1028
- ---
1029
-
1030
- **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
+ [![Coverage](https://img.shields.io/badge/coverage-95%25%2B-brightgreen)](https://github.com/lgriffin/ESI.ts)
8
+ [![npm downloads](https://img.shields.io/npm/dm/%40lgriffin/esi.ts)](https://www.npmjs.com/package/@lgriffin/esi.ts)
9
+ [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/lgriffin/ESI.ts/badge)](https://scorecard.dev/viewer/?uri=github.com/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
+ **[Documentation Site](https://lgriffin.github.io/ESI.ts/)** — guides, API reference, interactive endpoint explorer, and runnable examples.
14
+
15
+ **v9.5.2** — Supply chain security hardening: all GitHub Actions pinned by SHA, npm publish with SLSA provenance attestations, least-privilege workflow permissions, script injection prevention, and ETag cache cross-tenant isolation.
16
+
17
+ **v9.5.0** — Adds 12 new ESI endpoints: CosmeticsClient (SKINR licenses, components, design lookup), ParagonHubClient (marketplace listings with cursor pagination), plus detail endpoints for Mercenary Dens, Tactical Operations, Skyhooks, and Sovereignty Hubs.
18
+
19
+ **235 endpoint definitions — 206 from the public ESI OpenAPI spec, plus 29 for newer EVE features (Equinox sovereignty, orbital skyhooks, mercenary dens, access lists, freelance jobs, military campaigns, corporation projects, SKINR cosmetics, Paragon Hub marketplace). All exercisable endpoints validated against live Tranquility.**
20
+
21
+ ## Why ESI.ts vs. OpenAPI-Generated Clients?
22
+
23
+ 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.
24
+
25
+ ### What generators give you
26
+
27
+ - TypeScript interfaces from the OpenAPI spec
28
+ - Basic request/response typing
29
+ - A thin HTTP wrapper
30
+
31
+ ### What ESI.ts gives you on top of that
32
+
33
+ | Capability | openapi-typescript | ESI.ts |
34
+ | ------------------------------- | ------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
35
+ | **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 200 GET endpoints have schemas. Schema mismatches throw `EsiValidationError` immediately. |
36
+ | **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. |
37
+ | **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. |
38
+ | **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. |
39
+ | **Retry & resilience** | None. | Exponential backoff with jitter, circuit breaker (closed/open/half-open), automatic 401 token refresh with concurrent coalescing. |
40
+ | **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. |
41
+ | **Batch operations** | None. | `batch()` with bounded concurrency for GET fan-out, `batchPost()` with auto-chunking for large POST payloads. |
42
+ | **Domain knowledge** | None — generic HTTP client. | 39 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). |
43
+ | **Streaming pagination** | None. | 21 domain clients with 73+ `stream*` methods via `AsyncGenerator` — process large datasets page-by-page without loading everything into memory. |
44
+ | **Testing** | Whatever you write. | 171 test suites, 4,957 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). 52 runnable example scripts. |
45
+
46
+ ### The real problem with generated clients
47
+
48
+ The ESI OpenAPI spec is not a perfect source of truth. During live endpoint validation against the OpenAPI 3.1 spec, we discovered:
49
+
50
+ - `addContacts`, `editContacts`, and 4 UI endpoints document parameters as request body when ESI actually expects query parameters
51
+ - `deleteCharacterContacts` expects comma-separated contact IDs as a query param, not a JSON body
52
+ - Fleet wing/squad names have a 10-character limit not documented in the spec
53
+ - The `updateMailMetadata` endpoint uses the field name `read`, not `is_read`
54
+
55
+ A generated client faithfully reproduces these spec bugs. ESI.ts fixes them.
56
+
57
+ ## Installation
58
+
59
+ ```bash
60
+ npm install @lgriffin/esi.ts
61
+ ```
62
+
63
+ ### Building from Source
64
+
65
+ ```bash
66
+ git clone https://github.com/lgriffin/ESI.ts.git
67
+ cd ESI.ts
68
+ npm install # installs dependencies and compiles (via the prepare script)
69
+ ```
70
+
71
+ If you've already installed and just need to recompile:
72
+
73
+ ```bash
74
+ npm run build
75
+ ```
76
+
77
+ Verify everything works:
78
+
79
+ ```bash
80
+ npm run example:status # quick smoke test — checks ESI is reachable
81
+ npm test # run the full test suite (171 suites, 4,957 tests)
82
+ ```
83
+
84
+ ## Sub-path Exports
85
+
86
+ ESI.ts provides sub-path exports for targeted imports, reducing bundle size when you only need specific parts of the library:
87
+
88
+ ```typescript
89
+ // Zod schemas for runtime validation
90
+ import { MarketOrderSchema } from '@lgriffin/esi.ts/schemas';
91
+
92
+ // Error classes and type guards
93
+ import { EsiError, isRetryable } from '@lgriffin/esi.ts/errors';
94
+
95
+ // Test utilities
96
+ import { TestDataFactory } from '@lgriffin/esi.ts/testing';
97
+ ```
98
+
99
+ ## Static Data Export (SDE) Module
100
+
101
+ ESI.ts includes a standalone module for querying CCP's EVE Online Static Data Export — 102 YAML files loaded into in-memory Maps with 109 typed interfaces, Zod validation, and ~97 query methods. No database, no external services.
102
+
103
+ Reading SDE files needs two optional peer dependencies, which `npm install @lgriffin/esi.ts` does not install:
104
+
105
+ ```bash
106
+ npm install js-yaml # SdeDataProvider.fromDirectory and fromZip (parses the YAML)
107
+ npm install adm-zip # SdeDataProvider.fromZip (reads the ZIP archive)
108
+ ```
109
+
110
+ `@lgriffin/esi.ts/sde` loads without them, and `MemorySdeProvider` never needs them. A method that needs one that is missing throws an `SdeError` naming the package and the install command.
111
+
112
+ ```typescript
113
+ import { SdeDataProvider } from '@lgriffin/esi.ts/sde';
114
+
115
+ const sde = SdeDataProvider.fromDirectory('./sde-data');
116
+
117
+ const tritanium = sde.getType(34);
118
+ console.log(tritanium?.name); // "Tritanium"
119
+
120
+ const jita = sde.getSolarSystem(30000142);
121
+ const minerals = sde.getTypesByGroup(18);
122
+ const caldari = sde.getFaction(500001);
123
+
124
+ sde.close();
125
+ ```
126
+
127
+ Download SDE data with: `npx ts-node scripts/sde-ingest.ts --output sde-data`
128
+
129
+ | Document | Description |
130
+ | -------------------------------------------------- | --------------------------------------------------------------------------------------------- |
131
+ | [SDE README](src/sde/README.md) | Module overview, quick start, full API reference (~97 methods), entity coverage table |
132
+ | [Architecture](src/sde/docs/ARCHITECTURE.md) | C4 diagrams (context, container, component), data flow sequence, ER diagram, design decisions |
133
+ | [Usage Guide](src/sde/docs/USAGE.md) | Provider patterns, query examples, error handling |
134
+ | [Developer Guide](src/sde/docs/DEVELOPER_GUIDE.md) | Project structure, new entity checklist, field normalization, testing patterns |
135
+ | [API Contracts](src/sde/docs/API_CONTRACTS.md) | Complete method reference for all IStaticDataProvider methods |
136
+
137
+ ## Quick Start
138
+
139
+ ```typescript
140
+ import { EsiClient } from '@lgriffin/esi.ts';
141
+
142
+ const client = new EsiClient();
143
+
144
+ // Public data — no auth required
145
+ const alliances = await client.alliance.getAlliances();
146
+ const character = await client.characters.getCharacterPublicInfo(1689391488);
147
+ const system = await client.universe.getSystemById(30000142);
148
+ const prices = await client.market.getMarketPrices();
149
+
150
+ // Authenticated data — token read from ESI_ACCESS_TOKEN env var
151
+ const authedClient = new EsiClient();
152
+ const assets = await authedClient.assets.getCharacterAssets(characterId);
153
+ const wallet = await authedClient.wallet.getCharacterWallet(characterId);
154
+
155
+ // Clean up when done
156
+ await client.shutdown();
157
+ ```
158
+
159
+ ## Guides
160
+
161
+ The README orients; the guides are canonical. Each one opens with the [engineering charter](guides/CHARTER.md) requirements it implements.
162
+
163
+ | Guide | Covers |
164
+ | -------------------------------------------------- | ----------------------------------------------------------------------------------- |
165
+ | [Architecture](guides/ARCHITECTURE.md) | Layers, request path, caching, retry, rate limiting, circuit breaker, interceptors |
166
+ | [Design rules](guides/DESIGN-RULES.md) | Naming and schema conventions, adding an endpoint, adding a client, generated files |
167
+ | [Errors](guides/ERRORS.md) | Error classes, type guards, retryability, token refresh, safe mode |
168
+ | [Logging](guides/LOGGING.md) | `ILogger`, per-client loggers, pino, `ESI_LOG_LEVEL`, silencing in tests |
169
+ | [Pagination](guides/PAGINATION.md) | Offset and cursor pagination, `stream*`, `fetchAll*`, batch helpers |
170
+ | [Runtime validation](guides/RUNTIME-VALIDATION.md) | Zod response and request validation |
171
+ | [Security](guides/SECURITY.md) | Runtime defences and supply-chain controls ([policy](SECURITY.md)) |
172
+ | [Testing](guides/TESTING.md) | Test tiers, coverage, EARS specification |
173
+ | [Mutation testing](guides/MUTATION-TESTING.md) | Stryker configuration and scores |
174
+ | [Quality gates](guides/QUALITY-GATES.md) | What runs at commit, push, PR, nightly and release; every workflow and script |
175
+ | [Release](guides/RELEASE.md) | Cutting a release, changelog, provenance, signatures, supported versions |
176
+ | [Semantic versioning](guides/SEMVER.md) | What is public, major/minor/patch decisions, breaking-change commits, merge buttons |
177
+ | [OKF bundle](guides/OKF.md) | The generated Open Knowledge Format catalogue of ESI |
178
+ | [Documentation](guides/DOCUMENTATION.md) | Documentation surfaces and the TypeDoc reference |
179
+ | [Beads](guides/BEADS.md) | Issue tracking workflow |
180
+
181
+ ## Configuration
182
+
183
+ ```typescript
184
+ const client = new EsiClient({
185
+ clientId: 'my-app', // User-Agent identifier (default: 'esi-client')
186
+ accessToken: 'your-token', // EVE SSO token for authenticated endpoints
187
+ baseUrl: 'https://esi.evetech.net', // ESI base URL (default)
188
+ onTokenRefresh: async () => newToken, // Auto-refresh on 401 (optional)
189
+ language: 'en', // Accept-Language header: en, de, fr, ja, ru, zh, ko, es (default: none)
190
+ timeout: 30000, // Request timeout in ms (default: 30000)
191
+ retryConfig: {
192
+ maxRetries: 3, // Max retry attempts for transient errors (default: 3)
193
+ baseDelayMs: 1000, // Initial backoff delay (default: 1000)
194
+ maxDelayMs: 30000, // Maximum backoff delay (default: 30000)
195
+ retryMutations: false, // Retry POST/PUT/DELETE (default: false, GET only)
196
+ },
197
+ enableETagCache: true, // ETag caching (default: true)
198
+ etagCacheConfig: {
199
+ maxEntries: 1000, // Max cached responses (default: 1000)
200
+ defaultTtl: 300000, // Fallback TTL in ms (default: 5 min)
201
+ cleanupInterval: 60000, // Expired entry cleanup interval (default: 1 min)
202
+ },
203
+ validateResponse: true, // Runtime Zod validation of ESI responses (default: true)
204
+ validateRequest: false, // Opt-in request body Zod validation for POST/PUT/DELETE (default: false)
205
+ retryStrategy: customRetryStrategy, // Injectable IRetryStrategy (default: built-in exponential backoff)
206
+ enableCircuitBreaker: false, // Opt-in circuit breaker (default: false); circuitBreakerConfig is ignored unless true
207
+ circuitBreakerConfig: {
208
+ keyStrategy: 'resolved', // CB keying: 'resolved' (per-URL) or 'template' (per-route) (default: 'resolved')
209
+ cleanupIntervalMs: 3600000, // Stale circuit cleanup interval (default: disabled)
210
+ },
211
+ });
212
+ ```
213
+
214
+ Retry is enabled by default (`maxRetries: 3`). 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. Set `maxRetries: 0` to disable retry.
215
+
216
+ The access token can be updated at runtime:
217
+
218
+ ```typescript
219
+ client.setAccessToken('new-token');
220
+ ```
221
+
222
+ ## Authentication
223
+
224
+ Many ESI endpoints require an EVE SSO access token. There are three ways to provide one:
225
+
226
+ ### 1. Environment variable (recommended)
227
+
228
+ Set `ESI_ACCESS_TOKEN` in your environment or a `.env` file. The client reads it automatically — no token in source code.
229
+
230
+ ```bash
231
+ # Copy the example and fill in your token
232
+ cp .env.example .env
233
+ ```
234
+
235
+ ```env
236
+ ESI_ACCESS_TOKEN=your-eve-sso-access-token
237
+ ESI_CLIENT_ID=my-app-name
238
+ ```
239
+
240
+ If you use a `.env` loader like [dotenv](https://www.npmjs.com/package/dotenv), load it before creating the client:
241
+
242
+ ```typescript
243
+ import 'dotenv/config';
244
+ import { EsiClient } from '@lgriffin/esi.ts';
245
+
246
+ const client = new EsiClient();
247
+ // Token is picked up from process.env.ESI_ACCESS_TOKEN
248
+ ```
249
+
250
+ ### 2. Constructor parameter
251
+
252
+ Pass the token directly (useful for apps that manage tokens themselves):
253
+
254
+ ```typescript
255
+ const client = new EsiClient({ accessToken: token });
256
+ ```
257
+
258
+ ### 3. Runtime update
259
+
260
+ Set or refresh the token after construction:
261
+
262
+ ```typescript
263
+ client.setAccessToken(newToken);
264
+ ```
265
+
266
+ ### Getting an EVE SSO token
267
+
268
+ 1. Register an application at [EVE Developers](https://developers.eveonline.com/)
269
+ 2. Set a callback URL and select the ESI scopes your app needs
270
+ 3. Implement the [OAuth2 flow](https://docs.esi.evetech.net/docs/sso/) to obtain an access token
271
+ 4. Access tokens expire — use the refresh token to get new ones
272
+
273
+ ### Automatic Token Refresh
274
+
275
+ 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:
276
+
277
+ ```typescript
278
+ const client = new EsiClient({
279
+ accessToken: initialToken,
280
+ onTokenRefresh: async () => {
281
+ const response = await fetch('https://login.eveonline.com/v2/oauth/token', {
282
+ method: 'POST',
283
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
284
+ body: new URLSearchParams({
285
+ grant_type: 'refresh_token',
286
+ refresh_token: myRefreshToken,
287
+ client_id: myClientId,
288
+ }),
289
+ });
290
+ const { access_token } = await response.json();
291
+ return access_token;
292
+ },
293
+ });
294
+
295
+ // Requests now auto-refresh on 401 — no manual token management needed
296
+ const location = await client.location.getCharacterLocation(characterId);
297
+ ```
298
+
299
+ The token provider can also be set or changed at runtime:
300
+
301
+ ```typescript
302
+ client.setTokenProvider(myRefreshFunction);
303
+ client.setTokenProvider(undefined); // disable auto-refresh
304
+ ```
305
+
306
+ Key behaviors:
307
+
308
+ - Only retries **once** per request — if the refreshed token also gets a 401, the error is thrown
309
+ - **Concurrent coalescing** — if multiple requests hit 401 simultaneously, only one refresh call is made
310
+ - If the refresh callback throws (e.g., refresh token revoked), a `TOKEN_REFRESH_FAILED` error is raised
311
+ - Without a token provider, 401 errors throw immediately as before
312
+
313
+ ### Token Manager (multi-character, persistent)
314
+
315
+ The refresh callback above is the low-level hook. For applications that hold tokens for one or many characters, `EsiTokenManager` does the whole lifecycle: the SSO code exchange, persistence through a pluggable storage adapter, proactive refresh ahead of expiry, coalescing of concurrent refreshes, persistence of the rotated refresh token, revocation tracking, and bulk refresh with a concurrency cap.
316
+
317
+ ```typescript
318
+ import {
319
+ EsiTokenManager,
320
+ FileTokenStorage,
321
+ generateState,
322
+ } from '@lgriffin/esi.ts';
323
+
324
+ const tokens = new EsiTokenManager({
325
+ clientId: process.env.ESI_SSO_CLIENT_ID!,
326
+ clientSecret: process.env.ESI_SSO_CLIENT_SECRET, // omit for a public (PKCE) client
327
+ callbackUrl: 'https://my-app.example/callback',
328
+ storage: new FileTokenStorage('./tokens.json'), // or MemoryTokenStorage, or your own
329
+ });
330
+
331
+ // 1. Send the player to SSO
332
+ const state = generateState();
333
+ const loginUrl = tokens.getAuthorizationUrl({
334
+ scopes: ['esi-wallet.read_character_wallet.v1'],
335
+ state,
336
+ });
337
+
338
+ // 2. On the callback, exchange the code. The character id, name and scopes
339
+ // are decoded from the token; you never have to say who just logged in.
340
+ const stored = await tokens.addCharacter(codeFromCallback);
341
+ console.log(`Added ${stored.characterName} (${stored.characterId})`);
342
+
343
+ // 3. Get a client bound to that character. Its token is refreshed before
344
+ // expiry, and again on a 401, through the manager.
345
+ const client = await tokens.createClient(stored.characterId);
346
+ const wallet = await client.wallet.getCharacterWallet(stored.characterId);
347
+
348
+ // Or just the access token, for use elsewhere
349
+ const accessToken = await tokens.getToken(stored.characterId);
350
+ ```
351
+
352
+ Public clients (desktop and CLI tools that cannot keep a secret) use PKCE:
353
+
354
+ ```typescript
355
+ import { generatePkcePair } from '@lgriffin/esi.ts';
356
+
357
+ const pkce = generatePkcePair();
358
+ const loginUrl = tokens.getAuthorizationUrl({
359
+ scopes,
360
+ state,
361
+ codeChallenge: pkce.codeChallenge,
362
+ });
363
+ // ...later, on the callback:
364
+ await tokens.addCharacter(code, { codeVerifier: pkce.codeVerifier });
365
+ ```
366
+
367
+ #### Bulk refresh
368
+
369
+ Applications holding many characters (corporation tools, alliance services) refresh in bulk. Per-character failures never reject the call; each character gets its own result. The one exception is a storage adapter that cannot list tokens, which rejects with the storage error.
370
+
371
+ ```typescript
372
+ const results = await tokens.refreshAll({
373
+ concurrency: 5, // simultaneous SSO requests (default 5)
374
+ expiringWithinMs: 5 * 60_000, // only tokens expiring in the next 5 minutes; omit for all
375
+ });
376
+
377
+ for (const r of results) {
378
+ switch (r.status) {
379
+ case 'refreshed':
380
+ break;
381
+ case 'skipped':
382
+ break; // not stale, or the run was aborted
383
+ case 'revoked':
384
+ console.log(`${r.characterId} must log in again`);
385
+ break;
386
+ case 'failed':
387
+ if (r.retryable) scheduleRetry(r.characterId);
388
+ break;
389
+ }
390
+ }
391
+ ```
392
+
393
+ #### Storage adapters
394
+
395
+ `ITokenStorage` is four async methods keyed by character id: `get`, `set`, `delete`, `list`. Two adapters ship with the library:
396
+
397
+ | Adapter | Use for |
398
+ | -------------------- | ---------------------------------------------------------------------- |
399
+ | `MemoryTokenStorage` | Tests, CLIs that log in every run, a cache in front of a durable store |
400
+ | `FileTokenStorage` | Single-process apps; atomic temp-file-and-rename writes, `0600` mode |
401
+
402
+ Implement the interface over Redis, Postgres, or a keychain for anything else. One rule matters: `set` must be durable before it resolves, because the manager persists the rotated refresh token before returning the new access token, and SSO invalidates the previous one.
403
+
404
+ Key behaviors:
405
+
406
+ - **One token per character** — re-authorizing replaces the stored token rather than accumulating a second one; a warning is logged if the new consent drops scopes
407
+ - **Proactive refresh** — `getToken` refreshes when the token is inside `refreshSkewMs` of expiry (default 60 s), so requests are never sent with a token about to fail
408
+ - **Coalescing** — concurrent refreshes for the same character share one SSO call, which matters because SSO rotates the refresh token on every use
409
+ - **Revocation tracking** — an `invalid_grant` from SSO marks the character revoked; later calls throw `TokenRevokedError` locally instead of hitting SSO again
410
+ - **Hooks** — `onRefresh`, `onRefreshError`, and `onRevoked` for logging, metrics, or prompting a re-login
411
+ - **No JWT signature verification** — tokens are trusted because they arrive directly from SSO over TLS; do not use `decodeAccessToken` to authenticate tokens presented by third parties
412
+ - **Single process per store** — two processes sharing one `FileTokenStorage` would each rotate refresh tokens the other cannot see
413
+
414
+ ### Environment variables reference
415
+
416
+ | Variable | Description | Default |
417
+ | ------------------ | -------------------------------------------- | ------------------------- |
418
+ | `ESI_ACCESS_TOKEN` | EVE SSO access token | none |
419
+ | `ESI_CLIENT_ID` | User-Agent identifier | `esi-client` |
420
+ | `ESI_BASE_URL` | ESI API base URL | `https://esi.evetech.net` |
421
+ | `ESI_LOG_LEVEL` | Log level (`error`, `warn`, `info`, `debug`) | `warn` |
422
+
423
+ ## Available APIs
424
+
425
+ All clients are accessed as properties on the `EsiClient` instance. Authenticated endpoints require an access token.
426
+
427
+ | Client | Property | Auth | Examples |
428
+ | ------------------ | ---------------------------- | ---- | ------------------------------------------------------------------------------------- |
429
+ | Alliance | `client.alliance` | Some | `getAlliances()`, `getAllianceById(id)` |
430
+ | Assets | `client.assets` | Yes | `getCharacterAssets(id)` |
431
+ | Calendar | `client.calendar` | Yes | `getCalendarEvents(id)` |
432
+ | Characters | `client.characters` | Some | `getCharacterPublicInfo(id)`, `getCharacterPortrait(id)` |
433
+ | Clones | `client.clones` | Yes | `getCharacterClones(id)` |
434
+ | Contacts | `client.contacts` | Yes | `getCharacterContacts(id)`, `postCharacterContacts(id, standing, contactIds)` |
435
+ | Contracts | `client.contracts` | Yes | `getCharacterContracts(id)` |
436
+ | Corp Projects | `client.corporationProjects` | Yes | `getCorporationProjects(corpId)`, `getCorporationProject(corpId, projectId)` |
437
+ | Corporations | `client.corporations` | Some | `getCorporationInfo(id)`, `getCorporationMembers(id)` |
438
+ | Dogma | `client.dogma` | No | `getDogmaAttributes()`, `getDynamicItemInfo(typeId, itemId)` |
439
+ | Factions | `client.factions` | Some | `getFactionWarStats()` |
440
+ | Fittings | `client.fittings` | Yes | `getFittings(id)`, `createFitting(id, body)` |
441
+ | Fleets | `client.fleets` | Yes | `getFleetInformation(id)`, `getFleetMembers(id)` |
442
+ | Incursions | `client.incursions` | No | `getIncursions()` |
443
+ | Industry | `client.industry` | Some | `getCharacterIndustryJobs(id)` |
444
+ | Insurance | `client.insurance` | No | `getInsurancePrices()` |
445
+ | Killmails | `client.killmails` | Some | `getKillmail(id, hash)` |
446
+ | Location | `client.location` | Yes | `getCharacterLocation(id)` |
447
+ | Loyalty | `client.loyalty` | Yes | `getCharacterLoyaltyPoints(id)` |
448
+ | Mail | `client.mail` | Yes | `getCharacterMail(id)`, `sendMail(id, body)` |
449
+ | Market | `client.market` | Some | `getMarketPrices()`, `getMarketOrders(regionId)` |
450
+ | Military Campaigns | `client.militaryCampaigns` | Some | `getMilitaryCampaigns()`, `getMilitaryCampaignById(id)` |
451
+ | PI | `client.pi` | Yes | `getCharacterPlanets(id)` |
452
+ | Route | `client.route` | No | `getRoute(origin, destination)` |
453
+ | Search | `client.search` | Some | `search(characterId, query)` |
454
+ | Skills | `client.skills` | Yes | `getCharacterSkills(id)` |
455
+ | Sovereignty | `client.sovereignty` | No | `getSovereigntySystems()`, `getSovereigntyMap()` |
456
+ | Skyhooks | `client.skyhooks` | Some | `getSovereigntyHubs(corpId)`, `getSkyhookDetail(corpId, id)`, `getRaidableSkyhooks()` |
457
+ | Mercenary | `client.mercenary` | Yes | `getMercenaryDens(charId)`, `getMercenaryDenDetail(charId, denId)` |
458
+ | Cosmetics | `client.cosmetics` | Some | `getSkinr(id)`, `getCharacterSkinr(charId)`, `getCharacterSkinrComponents(charId)` |
459
+ | Paragon Hub | `client.paragonHub` | Some | `getPublicListings()`, `getCharacterListings(charId)`, `getAllianceListings(id)` |
460
+ | Access Lists | `client.accessLists` | Yes | `getAccessList(id)` |
461
+ | Status | `client.status` | No | `getStatus()` |
462
+ | UI | `client.ui` | Yes | `setAutopilotWaypoint(destId, addToBeginning, clear)`, `openNewMailWindow(body)` |
463
+ | Universe | `client.universe` | Some | `getSystemById(id)`, `getTypeById(id)` |
464
+ | Wallet | `client.wallet` | Yes | `getCharacterWallet(id)` |
465
+ | Wars | `client.wars` | No | `getWars()`, `getWarById(id)` |
466
+ | Freelance Jobs | `client.freelanceJobs` | Some | `getFreelanceJobs()`, `getFreelanceJobById(id)` |
467
+ | Meta | `client.meta` | No | `getOpenApiJson()`, `getOpenApiYaml()` |
468
+
469
+ ## Runtime Response Validation
470
+
471
+ 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.
472
+
473
+ 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.
474
+
475
+ ```typescript
476
+ import {
477
+ EsiClient,
478
+ EsiValidationError,
479
+ isValidationError,
480
+ schemas,
481
+ } from '@lgriffin/esi.ts';
482
+
483
+ const client = new EsiClient();
484
+
485
+ // Validation happens automatically on every request
486
+ const character = await client.characters.getCharacterPublicInfo(12345);
487
+
488
+ // Disable validation globally if needed
489
+ const rawClient = new EsiClient({ validateResponse: false });
490
+
491
+ // Use schemas directly for your own validation
492
+ const result = schemas.CharacterInfoSchema.safeParse(someData);
493
+ if (result.success) {
494
+ console.log(result.data.name);
495
+ }
496
+ ```
497
+
498
+ ### Request Body Validation
499
+
500
+ For POST/PUT/DELETE endpoints, opt-in request body validation ensures outgoing payloads match the endpoint's `requestSchema` before the request is sent:
501
+
502
+ ```typescript
503
+ // Opt-in request body validation for POST/PUT/DELETE
504
+ const client = new EsiClient({ validateRequest: true });
505
+
506
+ // Throws EsiValidationError if the request body doesn't match the endpoint's requestSchema
507
+ await client.mail.sendMail(characterId, {
508
+ recipients: [{ recipient_id: 12345, recipient_type: 'character' }],
509
+ subject: 'Hello',
510
+ body: 'Message body',
511
+ });
512
+ ```
513
+
514
+ See [guides/RUNTIME-VALIDATION.md](guides/RUNTIME-VALIDATION.md) for the full guide on schemas, error handling, and extending schemas.
515
+
516
+ ## Caching
517
+
518
+ ETag caching is on by default and works in three tiers: a GET inside the spec-defined TTL is answered from cache with no HTTP call, an older entry is revalidated with `If-None-Match`, and a 5xx with a cached copy serves the stale body instead of throwing. Authenticated cache entries are isolated per token.
519
+
520
+ ```typescript
521
+ const client = new EsiClient({ etagCacheConfig: { maxEntries: 2000 } });
522
+ client.getCacheStats();
523
+ client.clearCache();
524
+ ```
525
+
526
+ See [Caching in the architecture guide](guides/ARCHITECTURE.md#4-caching) for TTL precedence, invalidation, keys and configuration.
527
+
528
+ ## Batch Requests
529
+
530
+ Fetch data for multiple IDs with bounded concurrency using `batch()`, or chunk large POST payloads with `batchPost()`:
531
+
532
+ ```typescript
533
+ import { EsiClient } from '@lgriffin/esi.ts';
534
+
535
+ const client = new EsiClient();
536
+
537
+ // Fetch 500 type details with at most 10 concurrent requests (default 20)
538
+ const result = await client.batch(
539
+ typeIds,
540
+ (id) => client.universe.getTypeById(id),
541
+ {
542
+ concurrency: 10,
543
+ onProgress: (done, total) => console.log(`${done}/${total}`),
544
+ },
545
+ );
546
+
547
+ // result.results: Map<number, T> — successful responses
548
+ // result.errors: Map<number, Error> — failed requests
549
+ console.log(`${result.results.size} succeeded, ${result.errors.size} failed`);
550
+ ```
551
+
552
+ For POST endpoints that accept arrays (e.g., `postNamesAndCategories` with a 1000-ID limit), `batchPost` auto-chunks and concatenates:
553
+
554
+ ```typescript
555
+ const allNames = await client.batchPost(
556
+ largeIdArray,
557
+ (chunk) => client.universe.postNamesAndCategories(chunk),
558
+ 1000, // chunk size
559
+ );
560
+ ```
561
+
562
+ ## Streaming Pagination
563
+
564
+ Paginated endpoints can be consumed three ways: the plain method fetches every page and returns one array, `stream*` methods yield one validated page at a time, and `fetchAll*` methods fetch the remaining pages concurrently.
565
+
566
+ ```typescript
567
+ for await (const page of client.market.streamMarketOrders(10000002)) {
568
+ console.log(
569
+ `Page ${page.page}/${page.totalPages}: ${page.data.length} orders`,
570
+ );
571
+ if (page.page >= 3) break; // stops fetching the remaining pages
572
+ }
573
+ ```
574
+
575
+ Try it: `npm run example:streaming`. See [guides/PAGINATION.md](guides/PAGINATION.md) for the full method list, concurrency defaults and failure behaviour.
576
+
577
+ ## Cursor-based Pagination
578
+
579
+ Newer ESI routes such as Freelance Jobs page with opaque `before` / `after` cursor tokens instead of page numbers. `fetchAllCursorPages` follows them to the end of the dataset, and a saved `after` token can be polled later for changed records.
580
+
581
+ See [guides/PAGINATION.md](guides/PAGINATION.md) for cursor semantics and examples.
582
+
583
+ ## Generated Types
584
+
585
+ 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:
586
+
587
+ ```typescript
588
+ import { EsiSpec } from '@lgriffin/esi.ts';
589
+
590
+ // Generated type — uses OpenAPI schema names (v7.0.0+)
591
+ const order: EsiSpec.MarketsRegionIdOrdersGet = {
592
+ order_id: 123,
593
+ type_id: 34,
594
+ price: 5.5,
595
+ volume_remain: 1000,
596
+ volume_total: 5000,
597
+ is_buy_order: false,
598
+ // ...
599
+ };
600
+ ```
601
+
602
+ To regenerate types from the latest ESI spec:
603
+
604
+ ```bash
605
+ npm run generate:types # fetches OpenAPI spec, generates 161 interfaces + cache TTL map + rate limit groups + scope map
606
+ npm run validate:esi # reports type drift between hand-written and generated types
607
+ ```
608
+
609
+ ## ESI Scopes
610
+
611
+ 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:
612
+
613
+ ```typescript
614
+ import { esiEndpointScopes, EsiScope } from '@lgriffin/esi.ts';
615
+
616
+ // Look up scopes for a specific endpoint
617
+ const walletScopes = esiEndpointScopes['GET:characters/{character_id}/wallet'];
618
+ // → ['esi-wallet.read_character_wallet.v1']
619
+
620
+ // Check if an endpoint requires auth
621
+ const isPublic = !esiEndpointScopes['GET:universe/types/{type_id}'];
622
+ // → true (public endpoint, no scopes needed)
623
+
624
+ // Type-safe scope values
625
+ const scope: EsiScope = 'esi-assets.read_assets.v1';
626
+ ```
627
+
628
+ ## Error Handling
629
+
630
+ Failed calls throw `EsiError` (with `statusCode`, a sanitised `url` and `retryable`) or one of its subclasses, `TimeoutError` and `EsiValidationError`. An open circuit throws `CircuitOpenError`. Type guards such as `isRetryable`, `isTimeout`, `isValidationError` and `isCircuitOpen` narrow them, and `withSafeMode()` returns a result envelope instead of throwing.
631
+
632
+ ```typescript
633
+ import { EsiError, isCircuitOpen } from '@lgriffin/esi.ts';
634
+
635
+ try {
636
+ await client.alliance.getAllianceById(99999999);
637
+ } catch (err) {
638
+ if (isCircuitOpen(err)) console.log(`Retry in ${err.retryAfterMs} ms`);
639
+ else if (err instanceof EsiError) console.log(err.statusCode, err.retryable);
640
+ }
641
+ ```
642
+
643
+ See [guides/ERRORS.md](guides/ERRORS.md) for the class hierarchy, retryability rules and safe mode.
644
+
645
+ ## Response Metadata
646
+
647
+ Use `withMetadata()` to get response headers, cache status, rate limit info, and timing alongside the data:
648
+
649
+ ```typescript
650
+ const metaClient = client.alliance.withMetadata();
651
+ const result = await metaClient.getAllianceById(99000001);
652
+
653
+ console.log(result.data.name); // "Goonswarm Federation"
654
+ console.log(result.meta.fromCache); // true if served from cache
655
+ console.log(result.meta.cacheHitType); // 'spec-ttl' | 'etag-304' | 'stale-on-error'
656
+ console.log(result.meta.responseTimeMs); // milliseconds
657
+ console.log(result.meta.rateLimit); // { remaining, limit, used, group }
658
+ console.log(result.meta.requestId); // ESI request ID for debugging
659
+ ```
660
+
661
+ The `meta` object includes:
662
+
663
+ | Field | Type | Description |
664
+ | ---------------- | ------------------------ | ------------------------------------------------- |
665
+ | `headers` | `Record<string, string>` | Raw response headers |
666
+ | `fromCache` | `boolean` | Whether data was served from cache |
667
+ | `stale` | `boolean` | Whether cached data is stale (5xx fallback) |
668
+ | `cacheHitType` | `string?` | `'spec-ttl'`, `'etag-304'`, or `'stale-on-error'` |
669
+ | `rateLimit` | `RateLimitMeta?` | Rate limit status from ESI headers |
670
+ | `responseTimeMs` | `number?` | Request duration in milliseconds |
671
+ | `requestId` | `string?` | ESI request ID |
672
+ | `warning` | `object?` | ESI deprecation warning |
673
+
674
+ ## Rate Limiting
675
+
676
+ Rate limiting is always on and needs no configuration. Each ESI rate-limit group from the OpenAPI spec gets its own bucket, the limiter learns remaining tokens from ESI's response headers, and a 420 or 429 blocks only the affected group. Multi-character applications can give each token its own buckets:
677
+
678
+ ```typescript
679
+ const client = new EsiClient({
680
+ rateLimiterConfig: {
681
+ userKeyExtractor: (headers) => headers['Authorization'] ?? 'anon',
682
+ },
683
+ });
684
+ ```
685
+
686
+ See [Rate limiting in the architecture guide](guides/ARCHITECTURE.md#6-rate-limiting) for the throttling rules, per-endpoint overrides and monitoring. Retry, deduplication, the opt-in [circuit breaker](guides/ARCHITECTURE.md#7-circuit-breaker) and [request/response interceptors](guides/ARCHITECTURE.md#8-interceptors) are documented alongside it.
687
+
688
+ ## Lightweight Clients
689
+
690
+ 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.
691
+
692
+ If you only need a subset of APIs, use `CustomEsiClient` or `EsiClientBuilder` to load only what you need:
693
+
694
+ ```typescript
695
+ import { EsiClientBuilder } from '@lgriffin/esi.ts';
696
+
697
+ const client = new EsiClientBuilder()
698
+ .addClients(['market', 'universe', 'characters'])
699
+ .withClientId('my-trading-bot')
700
+ .withAccessToken('your-token')
701
+ .build();
702
+
703
+ const prices = await client.market?.getMarketPrices();
704
+ const system = await client.universe?.getSystemById(30000142);
705
+ ```
706
+
707
+ Or create standalone single-API clients:
708
+
709
+ ```typescript
710
+ import { EsiApiFactory } from '@lgriffin/esi.ts';
711
+
712
+ const marketClient = EsiApiFactory.createMarketClient({
713
+ clientId: 'price-checker',
714
+ });
715
+ const prices = await marketClient.getMarketPrices();
716
+ ```
717
+
718
+ ## Endpoint Coverage
719
+
720
+ All 235 endpoint definitions have been validated against live Tranquility using the **OpenAPI 3.1 spec** — 206 from the public ESI spec plus 29 for newer EVE features. Full output is captured in [`openapi.output.md`](openapi.output.md).
721
+
722
+ | Category | Endpoints | Method |
723
+ | --------------------------- | --------- | -------------------------------------------------- |
724
+ | Public GETs | 86 | 52 runnable example scripts with captured output |
725
+ | Authenticated GETs | 114 | Example scripts + live testing with EVE SSO tokens |
726
+ | Contacts (POST/PUT/DELETE) | 3 | Live create/edit/delete lifecycle |
727
+ | Fittings (POST/DELETE) | 2 | Live create/delete lifecycle |
728
+ | Mail (POST/PUT/DELETE) | 5 | Live send/label/metadata/delete lifecycle |
729
+ | UI (POST) | 5 | Live testing with EVE client running |
730
+ | Calendar (PUT) | 1 | Live RSVP to event |
731
+ | Fleet (GET/POST/PUT/DELETE) | 14 | Live fleet with fleet commander + squad members |
732
+ | Assets POST | 3 | Live asset location/name queries |
733
+ | CSPA (POST) | 1 | Live charge cost calculation |
734
+ | Dogma dynamic (GET) | 1 | Live mutaplasmid (Abyssal) item query |
735
+ | Universe POST helpers | 3 | Live name resolution and affiliation |
736
+ | Freelance Jobs (GET) | 4 | Live queries (graceful 404 for no active jobs) |
737
+
738
+ ## Examples
739
+
740
+ 52 runnable examples are in the `examples/` directory.
741
+
742
+ ### Public Endpoints (no auth needed)
743
+
744
+ ```bash
745
+ npm run example:status # Server status — quickest smoke test
746
+ npm run example:character # Character public info, portrait, corporation
747
+ npm run example:universe # Solar system, constellation, region, station
748
+ npm run example:market # Average prices + Tritanium price history
749
+ npm run example:alliance # Alliance info + member corporations
750
+ npm run example:route # Jita-to-Amarr route with system names
751
+ npm run example:wars # Recent wars with aggressor/defender details
752
+ npm run example:sovereignty # Nullsec sovereignty map + active campaigns
753
+ npm run example:industry # Industry facilities, cost indices, insurance
754
+ npm run example:incursions # Active incursions + faction warfare stats
755
+ npm run example:dogma # Item type details + dogma attributes
756
+ npm run example:contracts # Public region contracts + auction bids/items
757
+ npm run example:rate-limiting # Rate limiter & pagination demonstration
758
+ npm run example:cursor-pagination # Freelance Jobs with cursor pagination
759
+ npm run example:streaming # Streaming pagination for large datasets
760
+ npm run example:token-refresh # Automatic token refresh on 401
761
+ npm run example:universe-encyclopedia # Ancestries, bloodlines, races, celestials
762
+ npm run example:dogma-meta-sov # Dogma effects, sovereignty, meta endpoint
763
+ npm run example:faction-details # Faction warfare leaderboards and stats
764
+ ```
765
+
766
+ ### Authenticated Endpoints (require ESI_ACCESS_TOKEN)
767
+
768
+ ```bash
769
+ npm run example # Full character profile assembly
770
+ npm run example:wallet # Wallet balance, journal, transactions
771
+ npm run example:skills # Trained skills, queue, attributes
772
+ npm run example:assets # Asset inventory with bulk name lookup
773
+ npm run example:killmails # Recent killmails + full details
774
+ npm run example:fleet # Fleet info, members, wing/squad structure
775
+ npm run example:mail # Inbox headers, labels, mailing lists
776
+ npm run example:location # Current system, online status, ship
777
+ npm run example:fittings # Saved fittings + clone state + implants
778
+ npm run example:contacts # Contact list with standings + labels
779
+ npm run example:character-details # Blueprints, roles, standings, medals
780
+ npm run example:corporation-details # Corp members, divisions, structures
781
+ npm run example:calendar-search # Calendar events + character search
782
+ npm run example:loyalty-pi # Loyalty points + planetary interaction
783
+ npm run example:industry-mining # Industry jobs + mining ledger
784
+ npm run example:market-orders # Character/corp market orders
785
+ npm run example:corp-contracts-wallet # Corp contracts, contacts, wallets
786
+ ```
787
+
788
+ ### Write Operations (require specific scopes + caution)
789
+
790
+ ```bash
791
+ npm run example:write-ops # Contacts, fittings, mail, UI lifecycle tests
792
+ npm run example:universe-posts # Name resolution + character affiliation (public)
793
+ npm run example:freelance-jobs # Freelance job queries
794
+ ```
795
+
796
+ ### Parallel Requests
797
+
798
+ ```typescript
799
+ const [character, portrait, corp] = await Promise.all([
800
+ client.characters.getCharacterPublicInfo(characterId),
801
+ client.characters.getCharacterPortrait(characterId),
802
+ client.corporations.getCorporationInfo(corporationId),
803
+ ]);
804
+
805
+ console.log(`${character.name} [${corp.ticker}]`);
806
+ ```
807
+
808
+ ### Market Analysis
809
+
810
+ ```typescript
811
+ const [orders, history] = await Promise.all([
812
+ client.market.getMarketOrders(regionId),
813
+ client.market.getMarketHistory(regionId, typeId),
814
+ ]);
815
+
816
+ const buyOrders = orders.filter((o) => o.is_buy_order);
817
+ const sellOrders = orders.filter((o) => !o.is_buy_order);
818
+
819
+ console.log(`Best buy: ${Math.max(...buyOrders.map((o) => o.price))}`);
820
+ console.log(`Best sell: ${Math.min(...sellOrders.map((o) => o.price))}`);
821
+ ```
822
+
823
+ ## Resource Management
824
+
825
+ Always call `shutdown()` when you're done to clean up cache timers:
826
+
827
+ ```typescript
828
+ const client = new EsiClient();
829
+ try {
830
+ const status = await client.status.getStatus();
831
+ console.log(status.server_version);
832
+ } finally {
833
+ await client.shutdown();
834
+ }
835
+ ```
836
+
837
+ ## Testing
838
+
839
+ ESI.ts has a comprehensive multi-tier testing strategy with 171 suites and 4,957 tests:
840
+
841
+ | Tier | Tests | Purpose |
842
+ | -------------------------- | ---------------- | ------------------------------------------------------------------ |
843
+ | **TDD unit tests** | 130 files | Every client method, endpoint path, query param, and body format |
844
+ | **BDD scenario tests** | 41 feature files | Behavioral specifications in Gherkin (Given/When/Then) |
845
+ | **Mocked integration** | Full suite | Cross-layer request flow with jest-fetch-mock |
846
+ | **Live smoke tests** | 46 examples | Every endpoint against live Tranquility |
847
+ | **ESI spec contract** | 15 tests | Endpoint definitions validated against live OpenAPI spec |
848
+ | **Deep contract tests** | 8 categories | Path params, query params, body, auth, schemas, pagination vs spec |
849
+ | **Property-based fuzzing** | 601 tests | fast-check fuzzing of validation, URL construction, Zod schemas |
850
+ | **Mutation testing** | Stryker | Validates test suite kills code mutants |
851
+ | **Type-level tests** | tsd | Consumer API type correctness via tsd |
852
+ | **Gated auth tests** | 33 tests | Authenticated endpoints with real tokens |
853
+ | **Construction parity** | Per-surface | Verifies all client surfaces get identical middleware defaults |
854
+ | **Spec-alignment** | Type assertions | Ensures hand-written types align with generated OpenAPI types |
855
+
856
+ ```bash
857
+ npm test # Unit + BDD tests (171 suites, 4,957 tests)
858
+ npm run coverage # Tests with coverage report (thresholds enforced)
859
+ npm run bdd # BDD scenario tests only
860
+ npm run contract # Contract tests (skipped without ESI_LIVE_TESTS=true)
861
+ npm run fuzz # Property-based fuzz tests (601 tests)
862
+ npm run mutation # Mutation testing (Stryker)
863
+ npm run benchmark # Performance benchmark tests
864
+ npm run test:types # tsd consumer type tests
865
+ ```
866
+
867
+ Coverage: statements 98.37%, branches 95.14%, functions 96.09%, lines 98.17%. Thresholds enforced in CI: branches 80%, functions 75%, lines 90%, statements 90%.
868
+
869
+ See [guides/TESTING.md](guides/TESTING.md) for the full testing guide, and [guides/ARCHITECTURE.md](guides/ARCHITECTURE.md) for architecture diagrams.
870
+
871
+ ## Development
872
+
873
+ ### Prerequisites
874
+
875
+ - Node.js 18+
876
+ - npm
877
+
878
+ ### Code Quality Tools
879
+
880
+ The project uses a comprehensive suite of static analysis and code quality tools:
881
+
882
+ | Tool | Purpose | Command |
883
+ | ------------------------------------------------------------------------------------ | ------------------------------------------------------- | ------------------------------ |
884
+ | [ESLint](https://eslint.org/) | Linting with TypeScript, security, and code smell rules | `npm run lint` |
885
+ | [Prettier](https://prettier.io/) | Code formatting | `npm run format:check` |
886
+ | [knip](https://knip.dev/) | Dead code and unused export detection | `npm run knip` |
887
+ | [eslint-plugin-security](https://github.com/eslint-community/eslint-plugin-security) | Security anti-pattern detection | Integrated into `npm run lint` |
888
+ | [eslint-plugin-sonarjs](https://github.com/SonarSource/eslint-plugin-sonarjs) | Cognitive complexity and code smell detection | Integrated into `npm run lint` |
889
+ | [husky](https://typicode.github.io/husky/) | Git pre-commit hooks | Automatic on commit |
890
+ | [lint-staged](https://github.com/lint-staged/lint-staged) | Run linters on staged files only | Automatic on commit |
891
+ | [Redocly CLI](https://redocly.com/docs/cli/) | OpenAPI spec validation and linting | `npm run validate:spec` |
892
+
893
+ ### Available Scripts
894
+
895
+ ```bash
896
+ # Development
897
+ npm run build # Compile TypeScript
898
+ npm run lint # Run ESLint
899
+ npm run lint:fix # Run ESLint with auto-fix
900
+ npm run format # Format code with Prettier
901
+ npm run format:check # Check formatting without modifying
902
+
903
+ # Testing
904
+ npm test # Unit tests (171 suites, 4,957 tests)
905
+ npm run test:all # Unit + BDD + integration + fuzz + type tests
906
+ npm run coverage # Tests with coverage report (thresholds enforced)
907
+ npm run bdd # BDD scenario tests
908
+ ESI_LIVE_TESTS=true npm run contract:live # Deep contract tests against live ESI spec (fails without the variable)
909
+ npm run fuzz # Property-based fuzz tests (fast-check)
910
+ npm run mutation # Mutation testing (Stryker)
911
+ npm run benchmark # Performance benchmark tests
912
+ npm run test:types # Consumer type tests (tsd)
913
+ npm run mock:esi # Start Prism mock ESI server on port 4010
914
+
915
+ # Static Analysis
916
+ npm run knip # Detect dead code and unused exports
917
+ npm run validate:esi # Validate endpoints against live ESI OpenAPI spec
918
+ npm run validate:spec # Lint ESI OpenAPI spec with Redocly (structural + best practices)
919
+ npm run validate:auth-scopes # Auth/scope cross-validation
920
+ npm run schema:drift # Schema drift detection (hand-written vs OpenAPI spec)
921
+ npm run validate # Run all checks: lint, format, build, coverage, knip
922
+ npm run generate:types # Regenerate TypeScript interfaces from ESI OpenAPI spec
923
+ npm run generate:endpoints # Regenerate endpoint definitions from ESI OpenAPI spec
924
+ npm run generate:all # Run all generators (types + endpoints + OKF)
925
+ npm run generate:okf # Generate OKF knowledge bundle from ESI OpenAPI spec
926
+
927
+ # Documentation
928
+ npm run docs # Generate TypeDoc API documentation
929
+ npm run docs:serve # Serve docs locally on port 8080
930
+ ```
931
+
932
+ ### ESI Endpoint Validation
933
+
934
+ To verify that the codebase endpoint definitions match the live ESI OpenAPI spec:
935
+
936
+ ```bash
937
+ npm run validate:esi
938
+ ```
939
+
940
+ This fetches the ESI OpenAPI spec and reports:
941
+
942
+ - Endpoints in the codebase that are no longer in the ESI spec
943
+ - Endpoints in the ESI spec that the codebase doesn't cover
944
+ - HTTP method mismatches between codebase and spec
945
+
946
+ ### Pre-commit Hooks
947
+
948
+ 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`.
949
+
950
+ ### CI/CD
951
+
952
+ Every push runs lint, format, build, typecheck and unit tests; pull requests to `master` run the full matrix behind a single Quality Gate check. Actions are SHA-pinned, packages publish with npm provenance, and release assets are cosign-signed.
953
+
954
+ See [guides/QUALITY-GATES.md](guides/QUALITY-GATES.md) for the gate matrix and every workflow, and [guides/SECURITY.md](guides/SECURITY.md) for the supply-chain controls.
955
+
956
+ ## Contributing
957
+
958
+ 1. Fork the repository
959
+ 2. Create a feature branch
960
+ 3. Write tests for your changes
961
+ 4. Run `npm run validate` to check everything passes
962
+ 5. Open a Pull Request
963
+
964
+ Work is tracked with [beads](https://github.com/gastownhall/beads) (`bd`). Run
965
+ `bd ready` to see available work — see [guides/BEADS.md](guides/BEADS.md) for the
966
+ full workflow.
967
+
968
+ ## License
969
+
970
+ GPL-3.0-or-later - see the [LICENSE](LICENSE) file for details.
971
+
972
+ ---
973
+
974
+ **o7**