Files
eden/docs/esi-operations.md
T

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.