Skip to content
lgriffinPublic

About

A TypeScript implementation for Eve Onlines ESI

Resources

Contributing

Security policy

Stars

14 stars

Watchers

1 watching

Forks

Latest commit

 

History

1,611 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ESI.ts logo

ESI.ts

npm version License: GPL v3 TypeScript CI/CD Pipeline Coverage Mutation score Tests EARS requirements npm downloads OpenSSF Scorecard

A TypeScript client for the EVE Online ESI API, built as an engineered product rather than a generated wrapper. It covers every operation in the ESI OpenAPI specification. Every response is validated at runtime, and caching, rate limiting, retry and pagination follow the rules ESI actually enforces. The claims on this page are backed by a test suite that is itself tested.

Documentation site: lgriffin.github.io/ESI.ts, with these guides, a page for every runnable example, and the API reference generated from the TSDoc. The requirements dashboard lists every EARS requirement the client is held to, the scenarios that prove it and its verdict, and how to write a new one.

Release line. Version 12.4.0 is current on npm. 12.0 corrects five list methods and their detail schemas to the shapes ESI actually sends, which changes their return types; nothing else breaks. The 10.x line stays available for Node 18 and 20. What 12.0 changes · What 11.0 changed · Changelog

Install

npm install @lgriffin/esi.ts

You need Node.js 22.12 or later; on Node 18 or 20, stay on 10.x. TypeScript projects need TypeScript 5.4 or later, under node16, nodenext or bundler module resolution. The consumer contract checks every one of those combinations against the published tarball, as ES module and CommonJS.

Quick start

import { EsiClient, createConsoleLogger } from '@lgriffin/esi.ts';

const log = createConsoleLogger('info');
const client = new EsiClient({
  userAgent: 'my-app/1.0 (you@example.com)',
  logger: createConsoleLogger(), // library diagnostics, at ESI_LOG_LEVEL (default warn)
});
try {
  const status = await client.status.getStatus();
  log.info(`${status.players} pilots online`);
} finally {
  client.shutdown();
}

The examples here and in the guides log through an ILogger rather than console: log is the program's own output, and the client gets a logger of its own for library diagnostics. Logging has the details and how to plug in pino or anything else.

Public data needs no token. For character data, pass an EVE SSO access token, or set ESI_ACCESS_TOKEN, and the client attaches it only to the calls that declare a scope:

const character = await client.characters.getCharacterPublicInfo(characterId);
const prices = await client.market.getMarketPrices();

const authed = new EsiClient({ accessToken: token });
const wallet = await authed.wallet.getCharacterWallet(characterId);

Next: Using the client for configuration and every domain client, and Authentication for SSO, refresh and many characters.

Multiple characters

An application that acts for many characters has one relationship with ESI, so client.as(identity) returns the same domain clients sending as one identity. Every view shares the client's cache, rate limiter, error budget and circuit breaker, and each identity's authenticated cache entries stay apart.

import { EsiClient } from '@lgriffin/esi.ts';

const client = new EsiClient({ userAgent: 'my-app/1.0 (you@example.com)' });
const status = await client.status.getStatus();
const pilot = client.as(tokens.identity(characterId));
const wallet = await pilot.wallet.getCharacterWallet(characterId);

For a client that must never send a token, buildPublic() on the builder returns domain clients typed without their authenticated methods, so such a call does not compile:

import { EsiClientBuilder } from '@lgriffin/esi.ts';

const pub = new EsiClientBuilder()
  .addClients(['market', 'universe'])
  .withConfig({ userAgent: 'my-app/1.0 (you@example.com)' })
  .buildPublic();
const prices = await pub.market?.getMarketPrices();

Many characters has the identities (EsiTokenManager, a raw token, a TokenProvider), what the views share, and the move from tokens.createClient. The same engine also serves createEsi() from @lgriffin/esi.ts/client, a spec-shaped low-level surface whose calls mirror the URL (esi.as(identity).character(id).wallet.get()).

Testing your application without ESI

createMockTransport() from @lgriffin/esi.ts/testing stands in for the network in your own tests. Pass it as transport to EsiClient, EsiClientBuilder or EsiApiFactory.createClient (or to createEsi()). It answers requests from a table of routes and records what your code sent. Everything between your call and the transport is the real pipeline: URL building, token attachment, the cache, retry, pagination and Zod validation. So a test can catch a bug in your code or in ours, and this runs as written:

import { EsiClient, createConsoleLogger } from '@lgriffin/esi.ts';
import { createMockTransport } from '@lgriffin/esi.ts/testing';

const log = createConsoleLogger('info');

const transport = createMockTransport().respond({
  method: 'GET',
  path: '/characters/{character_id}/wallet',
  body: 1234567.89,
});
const client = new EsiClient({
  userAgent: 'my-app/1.0 (you@example.com)',
  accessToken: 'an-access-token',
  transport,
});
try {
  const wallet = await client.wallet.getCharacterWallet(2114794365);
  log.info(`Wallet: ${wallet}`); // Wallet: 1234567.89
  log.info(`Sent: ${transport.sent[0]?.headers['authorization']}`); // Sent: Bearer an-access-token
} finally {
  client.shutdown();
}

A request no route answers is rejected with an EsiConfigurationError naming the request, without a retry, and appears in transport.unrouted. Testing has the route options and the record.

What you get

Capability What ESI.ts does
Full coverage 39 domain clients and 235 routes. They cover all 235 operations in the ESI specification at compatibility date 2026-08-18, plus the specification documents themselves. spec:coverage fails the build if one is missing.
Runtime validation Every GET response is checked against a hand-written Zod schema. Unknown fields pass through, so an additive change from CCP never breaks you. A changed shape throws EsiValidationError instead of corrupting your data quietly.
Caching A GET inside ESI's cache window makes no HTTP call. Older entries are revalidated with ETags, and a 5xx serves the stale copy. A write invalidates the reads it affects. Keys are hashed per token.
Rate limiting One bucket per ESI rate-limit group: 46 of them, generated from the spec. The limiter learns from ESI's headers and honours Retry-After. A 420 or 429 blocks only its own group.
Resilience Exponential backoff with jitter, a single coalesced token refresh on 401, deduplication of identical in-flight GETs, and an opt-in circuit breaker. Each one is an interface you can replace.
Pagination Offset and cursor paging. stream* yields one validated page at a time, and fetchAll* fetches pages concurrently. batch and batchPost handle fan-out.
Authentication EVE SSO with PKCE, and a token manager that handles storage, proactive refresh, rotation, revocation and bulk refresh for many characters.
Static data ./sde answers offline queries over CCP's Static Data Export: 99 typed lookups with no database. It shares no code with the HTTP pipeline, and a lint rule enforces that. It is tested to the same bar as the client (scorecard).
Correct wire format Where the specification is wrong about how ESI reads a request (parameters in the query rather than the body, undocumented length limits), the definitions follow ESI. The specification documents this and live validation proved it.

The engineering stance

The project is run to a written engineering charter. It has 70 numbered requirements, each in the same EARS form as the test specification, and each with a status that says whether a machine enforces it: 55 are Enforced, 6 Practised, 8 Partial and 1 Gap. A gap is recorded, never hidden. Seven positions explain most of the choices:

  1. The custom clients are the product. ESI.ts is a library of hand-written, curated domain clients. Method names, argument shapes, wire fixes and validation strictness are product decisions. Generation serves the clients and never replaces them, and a gate fails any change that deprecates one (Client architecture).
  2. The OpenAPI spec is upstream. Types, cache TTLs, rate-limit groups, scopes and 235 typed operations are generated from it, and CI fails when they go stale. Drift reports keep the hand-written code honest against it.
  3. Tolerate additive change. New fields and enum members from CCP never break a consumer. A removal is a breaking change.
  4. Resilience is pluggable. Retry, rate limiting, circuit breaking, deduplication, caching and transport are interfaces, not imports.
  5. Secure by construction. HTTPS and a host allowlist are checked at construction, tokens are attached only where a scope is declared, URLs are redacted in every error, and cache keys are hashed per token. Each control is a test.
  6. The specification executes. Behaviour is written as EARS requirements in Gherkin, one per Rule:, and verified by scenarios that mock only the transport. The audit fails a pull request that weakens the wording.
  7. Verifiable supply chain. Every action is SHA-pinned and every token least-privilege. Releases carry npm provenance, a signed SBOM, cosign signatures and checksums.

By the numbers

Measured on master at 801998a (12.4.0) on 2026-10-28 with the commands shown; the mutation and Scorecard rows cite the run that produced them. TESTING.md has the full breakdown.

Measure Value Reproduce
Tests run offline 11,422 tests in 366 suites; 11,213 run, 0 failing (208 live tests skip without credentials or an SDE export, one without a build) npm test, fuzz, faults, contract:replay, test:integration
Unit, BDD and composition 338 suites, 9,631 tests in about three and a half minutes npm test
Coverage 99.2% statements, 96.3% branches, 98.0% functions, 99.4% lines npm run coverage (floors 90 / 80 / 75 / 90)
Executable specification 698 EARS requirements, 911 scenarios, 81 feature files npm run ears
Property and fuzz tests 1,276 tests; 10,000 runs a property nightly npm run fuzz
Transport fault catalogue 159 faults through the real pipeline npm run faults
Recorded ESI payloads 128 replay tests, re-recorded nightly with a drift PR npm run contract:replay
Mutation score 89.4% of 2,890 mutants killed by the unit suite across src/core, src/sde, src/auth, src/client and the construction surfaces; src/core 86.5% (nightly of 2026-10-28), src/auth 91.7%, src/client 98.0%, EsiClient/EsiClientBuilder 95.6%, every SDE directory above 93%; floors per directory, ratcheted nightly, changed files on every PR npm run mutation:ratchet
Static data (SDE) 156 EARS requirements and 223 scenarios in 23 feature files; at least 90% of mutants killed in every SDE directory by the unit suite and by the BDD scenarios alone (2026-10-08, #458); a real-data run against CCP's current export every night sde/TESTING.md
CI One required check (ci-success) over the full matrix; 29 workflows, 18 of them scheduled QUALITY-GATES.md
OpenSSF Scorecard 8.3 / 10 on 2026-09-28; the checks still open need settings only the maintainer can change SECURITY.md

Every tier has to prove it can fail: a negative fixture, a killed mutant or a caught fault. Every floor is a one-way ratchet.

Packages in the box

Import What it holds
@lgriffin/esi.ts EsiClient, EsiClientBuilder, EsiApiFactory, domain clients, auth, errors, generated types and scopes
@lgriffin/esi.ts/schemas The Zod response schemas
@lgriffin/esi.ts/errors Error classes and type guards, including the auth errors
@lgriffin/esi.ts/testing createMockTransport and TestDataFactory for your own tests
@lgriffin/esi.ts/client createEsi, the spec-shaped low-level surface: esi.public (authenticated calls do not compile), esi.as(identity)
@lgriffin/esi.ts/sde SdeDataProvider (YAML and ZIP, through the optional peers js-yaml and adm-zip) and MemorySdeProvider
@lgriffin/esi.ts/sde/memory MemorySdeProvider alone, with no file-system or parser code, for browsers and bundles
import { MarketOrderSchema } from '@lgriffin/esi.ts/schemas';
import { EsiError, isRetryable } from '@lgriffin/esi.ts/errors';
import { SdeDataProvider } from '@lgriffin/esi.ts/sde';
import { createConsoleLogger } from '@lgriffin/esi.ts';

const log = createConsoleLogger('info');

const sdeData = SdeDataProvider.fromDirectory('./sde-data');
log.info(`Type 34: ${sdeData.getType(34)?.name}`); // Type 34: Tritanium
sdeData.close();

Guides

The README orients and the guides are canonical. Each guide opens with the charter requirements it implements.

Using ESI.ts
Using the client Construction, configuration, every domain client, metadata, batching, examples, 11.0 and 12.0 changes
Authentication Tokens, refresh on 401, SSO with PKCE, the multi-character token manager
Many characters One runtime, esi.public, esi.as(identity), what the views share, moving from createClient
Pagination Offset and cursor paging, stream*, fetchAll*, failure behaviour
Errors Error classes, type guards, retryability, safe mode
Runtime validation Zod response and request validation
Logging ILogger, per-client loggers, pino, ESI_LOG_LEVEL
Static data (SDE) The offline Static Data Export module: its role, isolation, API (module README)
How it is built
Engineering charter The requirements the project holds itself to, with status and gap register
Roadmap Phases, definitions of done, the SDE programme, the release gate
Lean decisions Why it is run this way: every decision since v7, with value stream maps
Architecture Layers, ports, the request path, caching, retry, rate limiting, circuit breaker
Client architecture Custom clients are the product; the generated layer underneath; the plan
Design rules Naming, schemas, adding an endpoint or a client, generated files
Testing Every test tier, what it proves, how to run it
Mutation testing Stryker shards, floors and the ratchet
Quality gates What runs at commit, push, PR, nightly and release; every workflow
Security Runtime defences and supply-chain controls (policy)
Semantic versioning What is public; major, minor or patch; breaking-change commits
Release Cutting a release, changelog, provenance, signatures, support window
Audit The Phase 0 measured baseline for 11.0
OKF bundle The generated Open Knowledge Format catalogue of ESI
Documentation Documentation surfaces, checked examples, TypeDoc
Beads Issue tracking workflow

Examples

examples/ has 58 runnable scripts, each with an npm script. The public ones run against live ESI every night, and a failure opens an issue.

npm run example:status      # quickest smoke test, no token
npm run example:market      # prices and history
npm run example:streaming   # stream* over a large region
npm run example             # full character profile (ESI_ACCESS_TOKEN)

The full list is in USAGE.md, and the examples showcase has a page for each with its source, what it needs and the command that runs it.

Contributing

git clone https://github.com/lgriffin/ESI.ts.git && cd ESI.ts
npm ci
npm test                        # unit and BDD suites, about three minutes
npm run check:local -- --fast   # every offline CI tier, minus the build

CONTRIBUTING.md is the whole path from a fresh clone to a merged pull request, including the checks CI runs that a first pull request usually trips. Issues labelled good first issue are bounded and need no product decision. Every behaviour change starts with an EARS requirement and a failing scenario, every commit is a conventional commit classified by SEMVER.md, and no floor is ever lowered.

License

GPL-3.0-or-later. See LICENSE for the licence text and NOTICE for the copyright line and CCP's trademark notice; EVE Online is the property of CCP hf. and this project is not affiliated with or endorsed by them.


o7

About

A TypeScript implementation for Eve Onlines ESI

Resources

Contributing

Security policy

Stars

14 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages