Files
eden/docs/esi-operations.md

8.1 KiB

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:

{ 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, versioning, caching, and 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.