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 onPORTenv var, default5999. - Type-check:
npx tsc(tsconfig.jsonhasnoEmit: true— there is no build/compile step; the.tsfile is the shipped artifact). - Test:
npm test— runstest/argument-map.test.ts(built-innode:testrunner) to cross-checkargumentMap.jsonagainst the@tlecommunity/api-specOpenAPI 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 missingrequiredarg returns an error (code1002).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.