Proxy util for accessing TLE server methods via more standard conventions.
v2-api CLAUDE.md
5.2 kB
Markdown
at main

CLAUDE.md #

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

What this is #

An Express 5 proxy that lets clients call the legacy TLE (The Lacuna Expanse) Perl game backend with modern ergonomics: paths like POST /v2/empire/is_name_available and a plain named-argument JSON body like {"name": "1vasari"}, instead of hand-building positional JSON-RPC 2.0 requests. It translates the request, forwards it to the backend, and passes the backend's response straight back.

Commands #

Requires Node >= 24.0.0 and npm >= 10 (.npmrc sets engine-strict=true). .nvmrc pins Node 24.

  • Install: npm install
  • Run locally: node --experimental-strip-types index.ts (Node strips the TS types at runtime; on Node >= 23.6 the flag is unnecessary). Listens on PORT env var, default 5999.
  • Type-check: npx tsc (tsconfig.json has noEmit: true — there is no build/compile step; the .ts file is the shipped artifact).
  • Test: npm test — runs test/argument-map.test.ts (built-in node:test runner) to cross-check argumentMap.json against the @tlecommunity/api-spec OpenAPI spec.
  • Format check: npm run format:check
  • Format fix: npm run format:fix
  • Docker: docker build -t v2-api . (see deployment note below)

There is no lint beyond Prettier. Prettier config: 100-col width, single quotes, semicolons, proseWrap: always (wrap Markdown/prose too).

npm test / test/argument-map.test.ts #

Asserts argumentMap.json and the spec stay in sync: every map key is a real spec path; the three sections are mutually exclusive; every positional arg is a spec request-body property (and vice versa) with matching required flags; and every spec path has a map entry or is listed in EXEMPT_PATHS in the test. A newly added/renamed spec method fails the test until it is mapped in argumentMap.json (or exempted). CI (.gitea/workflows/build.yml) runs this plus format:check in a test job that build depends on.

Architecture #

Single route #

POST /v2/:module/:method (in index.ts) is the only endpoint. It builds a JSON-RPC 2.0 request via prepareBody() and forwards it to http://server:5000/:module — the backend hostname and port are hardcoded (there's a TODO pass real port through); server is expected to resolve on the deploy network (Docker/Tailscale).

prepareBody() — the core translation #

Given the request body, it produces either an RPCRequest or an RPCError. It accepts three input shapes: an already-formed JSON-RPC object (has a jsonrpc key — passed through), an array (used as positional params directly), or a plain named object (converted).

Named -> positional conversion is driven entirely by argumentMap.json, which has four top-level sections keyed by /module/method. Pick the section from the Perl signature in the server repo's lib/Lacuna/RPC/**/*.pm (the JSON-RPC dispatcher passes array params as positional args and flattens object params into key/value pairs):

  • positional (~1123 methods): ordered list of { name, required }. The named object's values are emitted in this order; a missing required arg returns an error (code 1002).
  • namedOnlyArgumentHash (3 methods, my ($self, %args) = @_): backend wants a bare hash — params: { session_id, ...args }.
  • namedOnlyArgumentListFirstPositionHash (9 methods, my ($self, $args) = @_ reading $args->{session_id}): a single hashref wrapped in a one-element array — params: [ { session_id, ...args } ].
  • namedOnlyArgumentListSecondPositionHash (3 methods, my ($self, $session_id, $opts) = @_): session id first, then a hashref — params: [ session_id, { ...args } ].

A method present in none of the sections is rejected with a loud error rather than guessing an argument order. Keep argumentMap.json exhaustive for every method the proxy is expected to serve.

Auth #

Clients send Authorization: Token <session_id>. The token is injected as the session id: merged in as session_id for hash-style methods, or prepended as the first positional arg for positional methods. Methods called before a session exists (sessionlessMethods in index.ts: /empire/login, /empire/create, /empire/found, /empire/send_password_reset_message) never get a session id injected.

Errors #

errorCodes in index.ts enumerates the legacy game error codes (1000–1200). Validation and processing failures in the proxy return HTTP 500 with a JSON-RPC error body (usually code 1002); backend responses are relayed with the backend's own status code.

Regenerating argumentMap.json #

It was generated from the server repo's lib/Lacuna/RPC/**/*.pm plus the OpenAPI spec (~1157 methods; ~35 couldn't be auto-resolved — consolidated building names like beach, plus spec/backend drift). There is no generator script in this repo. Edit the file directly as gaps surface through testing.

Deployment #

Pushing to main triggers Gitea Actions (.gitea/workflows/build.yml): it joins a Tailscale network, builds a linux/amd64 image, and pushes gitea.allosaurus-chromatic.ts.net/tlecommunity/v2-api:latest to the private registry.