# 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 `. 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.