# ui/core — Galaxy Client Compute Module

`ui/core` (Go module `galaxy/core`) is the compute boundary of the
Galaxy cross-platform UI client. It carries v1 transport-envelope
canonical bytes, signature verification, and Ed25519 keypair
helpers. Network I/O and persistent storage live elsewhere on
purpose: this module compiles unchanged to WASM (the web target today)
and is designed to also compile to gomobile (mobile) and Wails-embedded
native (desktop) — both planned, see [`../ROADMAP.md`](../ROADMAP.md).

The authoritative byte contract is defined in
[`docs/ARCHITECTURE.md` §15](../../docs/ARCHITECTURE.md). The gateway
mirrors this exact wire format in its own
[`gateway/authn`](../../gateway/authn) package; cross-module byte
parity and round-trip sign/verify are exercised by
[`gateway/authn/parity_with_ui_core_test.go`](../../gateway/authn/parity_with_ui_core_test.go).

## Invariants

- **No network.** No `net/http`, no `net/url`, no gRPC client.
- **No storage.** No `os` (outside `_test.go` fixtures), no SQL, no
  filesystem, no keychain.
- **TinyGo-friendly.** Production files do not import
  `crypto/x509`, `encoding/pem`, or any package not supported by
  the WASM target. PKCS#8 PEM is server-only and stays in
  `gateway/authn`.
- **No goroutines, no channels, no `sync` primitives** in
  production files. Pure functions, deterministic outputs.
- **No re-export of `crypto/ed25519` types** in the public API.
  Callers see opaque `[]byte` blobs and `string` representations
  so the WASM bridge can hand them across the JS boundary as
  `Uint8Array` and string primitives.
- **Randomness is injected.** `keypair.Generate(reader io.Reader)`
  takes a caller-supplied reader. Production code passes
  `crypto/rand.Reader`; tests use deterministic `bytes.NewReader`;
  WASM later passes a `crypto.getRandomValues` adapter.

## Layout

```text
ui/core/
├── go.mod               module galaxy/core (Go 1.26.0)
├── calc/                thin wrappers over `pkg/calc/` (no math here)
│   ├── ship.go          ship geometry, mass, speed, combat
│   ├── planet.go        ship build cost / per-turn production
│   ├── solve.go         goal-seek inverse solvers
│   └── number.go        display rounding (Ceil3)
├── canon/               canonical-bytes builders and verifiers
│   ├── canon.go         length-prefix helpers
│   ├── request.go       galaxy-request-v1 fields and signing input
│   ├── response.go      galaxy-response-v1 fields and verifier
│   ├── event.go         galaxy-event-v1 fields and verifier
│   ├── signature.go     base64 client-key request verification
│   └── testdata/        committed JSON golden vectors
├── keypair/             Ed25519 generate / sign / verify / marshal
└── types/               full transport envelopes + result codes
```

## Public API

### `galaxy/core/canon`

- `RequestDomainMarkerV1`, `ResponseDomainMarkerV1`, `EventDomainMarkerV1`
  — UTF-8 domain prefixes that bind a signature to a specific
  envelope kind.
- `RequestSigningFields`, `ResponseSigningFields`, `EventSigningFields`
  — exact subsets of envelope fields covered by the v1 signature.
- `BuildRequestSigningInput`, `BuildResponseSigningInput`,
  `BuildEventSigningInput` — produce canonical bytes ready for
  `ed25519.Sign` / `ed25519.Verify`.
- `VerifyRequestSignature(clientPublicKey string, signature []byte,
  fields RequestSigningFields) error` — accepts the base64
  string form the backend stores in the device session.
- `VerifyResponseSignature(publicKey ed25519.PublicKey, signature []byte,
  fields ResponseSigningFields) error`,
  `VerifyEventSignature(publicKey ed25519.PublicKey, signature []byte,
  fields EventSigningFields) error` — used by the client to
  validate server output.
- `VerifyPayloadHash(payloadBytes, payloadHash []byte) error`.
- Sentinel errors: `ErrInvalidPayloadHash`, `ErrPayloadHashMismatch`,
  `ErrInvalidClientPublicKey`, `ErrInvalidRequestSignature`,
  `ErrInvalidResponseSignature`, `ErrInvalidEventSignature`.

### `galaxy/core/keypair`

- `Generate(reader io.Reader) (privateKey, publicKey []byte, err error)`.
- `Sign(privateKey, message []byte) ([]byte, error)` — returns a 64-byte
  raw Ed25519 signature.
- `Verify(publicKey, message, signature []byte) bool`.
- `MarshalPublicKey(publicKey []byte) (string, error)` — base64
  StdEncoding, the wire format documented in §15.
- `UnmarshalPublicKey(value string) ([]byte, error)`.
- `PublicKeyFromPrivate(privateKey []byte) ([]byte, error)`.
- Sentinel errors: `ErrInvalidPrivateKey`, `ErrInvalidPublicKey`,
  `ErrInvalidPublicKeyEncoding`.

### `galaxy/core/calc`

Thin Go bridges over `pkg/calc/`, surfaced via WASM to the ship-class
calculator and the ship-group inspector. Each function is a one-line
passthrough — no math lives here. Coverage spans ship geometry / mass /
speed / cargo, combat (attack, defence, bombing), block-upgrade and
per-turn ship-build cost, the single-target goal-seek inverse solvers,
and display rounding (`Ceil3`).

The authoritative function surface (which UI feature uses what, parity
rules, what is still deferred) lives in
[`ui/docs/calc-bridge.md`](../docs/calc-bridge.md).

### `galaxy/core/types`

- `RequestEnvelope`, `ResponseEnvelope`, `EventEnvelope` — full Go
  envelope structs mirroring the protobuf messages in
  `gateway/proto/galaxy/gateway/v1/`. Each exposes a
  `SigningFields()` method to project onto the corresponding
  `canon.*SigningFields`.
- `ProtocolVersionV1 = "v1"`, `ResultCodeOK = "ok"` — the only
  result string that is part of the stable client contract; any
  other `result_code` is downstream-opaque and must not be
  hard-coded by clients.

## Testing

```sh
go test -count=1 ./ui/core/...
```

The `canon` test suite combines:

- byte-equality on golden JSON fixtures under
  `canon/testdata/` for three request types
  (`user.account.get`, `lobby.my.games.list`,
  `user.games.order`), one response (`ok`), and one event
  (`gateway.server_time`);
- mutation tests proving every signed field is bound into the
  signature;
- round-trip sign-then-verify across all three envelope kinds;
- negative tests for tampered hashes, mismatched timestamps and
  request IDs, invalid signature lengths, and bad public-key
  encodings.

Cross-module parity (gateway accepts ui/core signatures and vice
versa) is enforced from
`gateway/authn/parity_with_ui_core_test.go`.

## What this module is **not**

- Not a network client. ConnectRPC over `@connectrpc/connect-web`
  on the TypeScript side is the only network surface.
- Not a key store. Per-platform secure storage lives in the platform
  `KeyStore` / `Cache` layer on the TypeScript side.
- Not a freshness gate. Server-side `±5 min` freshness checks
  remain in `gateway/internal/grpcapi/freshness_replay.go`. The
  client is expected to stamp its own `timestamp_ms` accurately
  via `time.Now`, but does not enforce a window.
- Not a FlatBuffers codec — report decoding lives on the TypeScript
  side, so this module stays small on purpose.

## Cross-references

- [`../../docs/ARCHITECTURE.md` §15](../../docs/ARCHITECTURE.md) —
  authoritative byte contract.
- [`../../gateway/authn`](../../gateway/authn) — server mirror of
  the same canonical bytes.
- [`../PLAN.md`](../PLAN.md) — the staged plan that describes how this
  module fits into the wider client.
