132 lines
8.1 KiB
Markdown
132 lines
8.1 KiB
Markdown
# ESI operations
|
|
|
|
The server ESI adapter uses the fixed trusted base `https://esi.evetech.net` and pins
|
|
`X-Compatibility-Date: 2026-08-18` (`ESI_COMPATIBILITY_DATE`). It exposes a fixed
|
|
allowlist of typed safe reads: character assets (paged GET), character asset names
|
|
and locations (bounded read-only POST), character wallet balance (unpaged GET), and
|
|
global market prices (unpaged GET), and character-scoped private structure
|
|
resolution (fixed GET). Callers cannot provide an arbitrary ESI URL, host, method,
|
|
or POST body.
|
|
|
|
## Asset synchronization
|
|
|
|
`EsiClient.getAssets(characterId, accessToken, authorizationKey)` fetches page one,
|
|
reads `X-Pages`, fetches all remaining pages, de-duplicates by `item_id` while
|
|
checking for duplicates, and revalidates page one after the other pages. A changed
|
|
page count, duplicate ID, malformed page, failed request, or changed first page
|
|
fails the operation. The caller must not persist a returned partial result: the
|
|
method returns only a complete bounded-read snapshot with
|
|
`consistency: "bounded-non-atomic"`. Asset IDs are retained as decimal strings,
|
|
and the response is parsed with `lossless-json` so an int64 cannot be rounded by
|
|
JavaScript.
|
|
|
|
This is a stability check, not a transaction. ESI's paginated endpoint does not
|
|
promise a true multi-page snapshot while its cache expires between page requests.
|
|
Only page one is conditionally revalidated: revalidating every page would multiply
|
|
remote calls up to the bounded page cap and still could not make ESI's independently
|
|
served pages atomic. The explicit `bounded-non-atomic` coverage state prevents this
|
|
adapter from claiming transactional consistency. A caller should stage pages and
|
|
replace the persisted Convex snapshot only after `getAssets` resolves. Convex
|
|
persistence is intentionally outside this adapter.
|
|
|
|
The cache is private to an `EsiClient` and keyed by a structured authorization
|
|
context: application user/principal, character, sorted scopes, `grantRevision`,
|
|
`authorizationEpoch`, and committed `credentialVersion`. Production callers pass
|
|
the current counters:
|
|
|
|
```ts
|
|
{ userId, principal, characterId, scopes, grantRevision, authorizationEpoch, credentialVersion }
|
|
```
|
|
|
|
Tokens are never used as cache keys or emitted in metrics. Private reads reject
|
|
legacy string keys and require all structured fields, including the matching
|
|
character, required scope, `grantRevision`, `authorizationEpoch`, and
|
|
`credentialVersion`. Changing any authority counter therefore cannot reuse a prior
|
|
private body or snapshot.
|
|
|
|
Responses retain their raw text, ETag, Last-Modified, and cache metadata. Fresh
|
|
responses use the server TTL; stale responses send `If-None-Match` and
|
|
`If-Modified-Since`, and a 304 reuses the cached body for both GET and declared
|
|
read-only POST operations. Canonical helper bodies are sorted decimal int64 JSON
|
|
literals, so ordering variants share one cache identity; operation/method/body
|
|
cannot collide. Helper requests accept 1..1000 unique positive signed-int64 IDs
|
|
and reject unsafe/duplicate/oversize input without Number coercion. Each operation
|
|
also enforces a response `Content-Length`/stream byte cap and result-row cap before
|
|
retaining a cache entry; asset pages and aggregate snapshots have separate bounds
|
|
(1,000 pages, 5,000 rows/page, 50,000 rows/snapshot). `Cache-Control`, `Expires`,
|
|
`Date`, and `Age` are honored. `no-store` responses retain neither body nor
|
|
validators. Requests have one deadline covering response-body reads and bounded
|
|
retries for transient network/5xx/420/429 responses. `Retry-After` (seconds or
|
|
HTTP date) and the legacy ESI error-limit reset are respected. Exhausted new
|
|
limiter buckets are cooled down per authorization context and group; the legacy
|
|
error limiter is cooled down globally. Query a private cooldown with
|
|
`client.getCooldown(authorizationKey, characterId)` or the public limiter with
|
|
`client.getPublicCooldown()`. A server delay that exceeds the remaining request
|
|
budget returns a typed rate-limit error with `retryAt`, `retryAfterMs`, and
|
|
scheduler-readable `cooldown` rather than retrying early. Response cancellation is
|
|
abort-first and non-blocking, so cleanup cannot extend a deadline. Process-local
|
|
retention is bounded by weighted response bytes and aggregate snapshot rows;
|
|
`client.getCacheStats()` exposes current usage and both budgets. Eviction is
|
|
oldest-insertion deterministic. 401 and 403 are never blindly retried. OAuth exchange/refresh remain in `EveSso` and never use
|
|
these safe-read retries.
|
|
|
|
## Asset names and locations
|
|
|
|
`EsiClient.getAssetNames` and `getAssetLocations` call fixed character helper
|
|
routes with the asset scope. Their outbound body is canonicalized and serialized
|
|
as numeric JSON literals (including IDs above `2^53`). Typed observations include
|
|
requested IDs, returned values, explicit `missing` IDs, `complete`, and
|
|
retrieval/expiry provenance. A missing helper result is never silently represented
|
|
as complete enrichment. Duplicate or unexpected IDs, malformed names, non-finite
|
|
coordinates, and malformed arrays fail closed. Deterministic transport fixtures live
|
|
under `tests/fixtures/esi/` so int64 names, finite positions, signed balances,
|
|
and versioned route/scope metadata are parsed from raw JSON rather than rounded
|
|
JavaScript literals. Asset helper metadata cites pinned OpenAPI evidence; wallet
|
|
metadata cites the current official OpenAPI retrieval and explicitly records that
|
|
it is not an immutable historical capture.
|
|
|
|
## Private structure resolution
|
|
|
|
`EsiClient.getStructure` calls the pinned `GET /universe/structures/{structure_id}`
|
|
operation with `esi-universe.read_structures.v1`. The route and scope are pinned in
|
|
`docs/research/eve.md` and the captured OpenAPI evidence: a 200 is character-scoped
|
|
ESI access evidence, while 403 and 404 remain explicit unresolved outcomes. IDs are
|
|
validated and serialized as decimal text without `Number` coercion. The response
|
|
retains the optional name, solar-system, owner/type IDs and finite position with
|
|
retrieval/expiry provenance. The T08 cache includes the complete authority and
|
|
reference version; it never stores tokens and never derives ACL membership or
|
|
docking permission (`unknown` is the only permission value). Deadlines, rate limits,
|
|
and transport failures remain typed errors for T07 retry/cooldown handling.
|
|
|
|
## Wallet balance
|
|
|
|
`EsiClient.getWalletBalance` reads the unpaged character wallet route with the
|
|
wallet scope. The adapter returns only a transport-level observation with signed
|
|
integer cents in `amount` (via T04's half-away-from-zero `decimalIskToCents`
|
|
normalization) and `retrievedAt` / `expiresAt`; sub-cent source values are rounded
|
|
by that approved policy, not rejected. Persistence, scheduling, and UI/service
|
|
orchestration belong to later work. Negative and very large balances remain
|
|
lossless, while malformed/non-finite numeric bodies are rejected. The tracked
|
|
fixture metadata records the official OpenAPI URL, operation ID, 120-second
|
|
TTL, `char-wallet` limiter, response SHA-256
|
|
`9a9a0f4af77216300e960cfd2576c9baac085a8170022e40c928c28622d80fc6`, and the
|
|
2026-09-19T19:30:12Z retrieval. This is a current retrieval using the pinned
|
|
compatibility date, not an immutable historical capture from 2026-08-18. Official
|
|
references: [ESI OpenAPI](https://esi.evetech.net/meta/openapi.json?compatibility_date=2026-08-18),
|
|
[versioning](https://developers.eveonline.com/docs/services/esi/overview/),
|
|
[caching](https://developers.eveonline.com/docs/services/esi/overview/#caching),
|
|
and [rate limiting](https://developers.eveonline.com/docs/services/esi/rate-limiting/).
|
|
|
|
## Market estimates
|
|
|
|
`EsiMarketEstimates` reads public `GET /markets/prices`. It uses only
|
|
`average_price`; a missing average price is omitted and `adjusted_price` is never
|
|
a fallback. The returned `PriceSnapshot.source` includes the pinned compatibility
|
|
date, `asOf` is the retrieval time (ESI supplies no source timestamp), and prices
|
|
are integer ISK cents represented as validated decimal strings. The route is a
|
|
cached global reference estimate, not a regional or structure order book.
|
|
|
|
Metrics supplied through `EsiClient`'s `observe` callback include endpoint, status,
|
|
retry/cache state, and redacted rate-limit headers. They do not include access
|
|
tokens, authorization keys, or response bodies.
|