MVP web client (Phases 1-30) is complete; reorganize planning + living docs around that. - PLAN.md kept as the staged MVP record (1-30) with a status block + pointers; removed the 31-36 stages, regression scenarios, and deferred-TODO section (moved out); fixed a stale cross-machine plan path. - ui/PLAN-finalize.md (new): active web-finalization plan in 8 stages (visual system, a11y, i18n, error UX, PWA, build hygiene, docs, owner manual-QA loop); absorbs former Phases 33 and 35. - ui/ROADMAP.md (new): post-MVP (Wails, Capacitor, realistic projection, acceptance + regression scenarios) and triaged deferred follow-ups. - ui/docs/README.md (new): grouped topic-doc index. - De-archaeologized all 20 ui/docs topic docs + ui/README.md + ui/core/README.md: stripped Phase-N build history, rewritten as current-state; deferred work now points at ROADMAP.md / PLAN-finalize.md. Docs-only; no code change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
The authoritative byte contract is defined in
docs/ARCHITECTURE.md §15. The gateway
mirrors this exact wire format in its own
gateway/authn package; cross-module byte
parity and round-trip sign/verify are exercised by
gateway/authn/parity_with_ui_core_test.go.
Invariants
- No network. No
net/http, nonet/url, no gRPC client. - No storage. No
os(outside_test.gofixtures), 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 ingateway/authn. - No goroutines, no channels, no
syncprimitives in production files. Pure functions, deterministic outputs. - No re-export of
crypto/ed25519types in the public API. Callers see opaque[]byteblobs andstringrepresentations so the WASM bridge can hand them across the JS boundary asUint8Arrayand string primitives. - Randomness is injected.
keypair.Generate(reader io.Reader)takes a caller-supplied reader. Production code passescrypto/rand.Reader; tests use deterministicbytes.NewReader; WASM later passes acrypto.getRandomValuesadapter.
Layout
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 fored25519.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.
galaxy/core/types
RequestEnvelope,ResponseEnvelope,EventEnvelope— full Go envelope structs mirroring the protobuf messages ingateway/proto/galaxy/gateway/v1/. Each exposes aSigningFields()method to project onto the correspondingcanon.*SigningFields.ProtocolVersionV1 = "v1",ResultCodeOK = "ok"— the only result string that is part of the stable client contract; any otherresult_codeis downstream-opaque and must not be hard-coded by clients.
Testing
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-webon the TypeScript side is the only network surface. - Not a key store. Per-platform secure storage lives in the platform
KeyStore/Cachelayer on the TypeScript side. - Not a freshness gate. Server-side
±5 minfreshness checks remain ingateway/internal/grpcapi/freshness_replay.go. The client is expected to stamp its owntimestamp_msaccurately viatime.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 — authoritative byte contract.../../gateway/authn— server mirror of the same canonical bytes.../PLAN.md— the staged plan that describes how this module fits into the wider client.