# 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 (Phase 5), gomobile (Phase 32), and Wails-embedded native (Phase 31). 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) ├── 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/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.command`), 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 (Phase 5+). - Not a key store. Per-platform secure storage lives in Phase 6. - 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 — that lands in a later phase, so the module today is 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) Phase 3 — the staged plan that describes how this module fits into the wider client.