diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..7e11055 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.wasm binary diff --git a/go.work b/go.work index 1bf0e01..42429c8 100644 --- a/go.work +++ b/go.work @@ -19,6 +19,7 @@ use ( ./pkg/transcoder ./pkg/util ./ui/core + ./ui/wasm ) replace ( diff --git a/ui/.gitignore b/ui/.gitignore index 8b12030..6db088c 100644 --- a/ui/.gitignore +++ b/ui/.gitignore @@ -7,8 +7,12 @@ node_modules/ build/ dist/ -# Generated WASM bundles +# Generated WASM bundles. The committed `frontend/static/core.wasm` +# (built by `make wasm` from `ui/wasm/`) is intentionally tracked so +# Vitest and the SvelteKit dev server have the artefact available +# without forcing every contributor to install TinyGo locally. *.wasm +!frontend/static/core.wasm # Wails desktop wrapper (Phase 31+) desktop/build/ diff --git a/ui/Makefile b/ui/Makefile index 02e9cea..38d202d 100644 --- a/ui/Makefile +++ b/ui/Makefile @@ -1,11 +1,16 @@ -.PHONY: help web wasm gomobile desktop-mac desktop-win desktop-linux ios android all +.PHONY: help web wasm ts-protos gomobile desktop-mac desktop-win desktop-linux ios android all .DEFAULT_GOAL := help +WASM_OUT := frontend/static/core.wasm +WASM_EXEC := frontend/static/wasm_exec.js +TINYGO_ROOT := $(shell tinygo env TINYGOROOT 2>/dev/null) + help: - @echo "ui targets (placeholders, implemented in later phases of ui/PLAN.md):" + @echo "ui targets:" + @echo " wasm TinyGo build of ui/core to core.wasm + wasm_exec.js shim (Phase 5)" + @echo " ts-protos Connect-ES + Protobuf-ES generation from gateway/proto (Phase 5)" @echo " web Vite production build (Phase 5+)" - @echo " wasm TinyGo build of ui/core to core.wasm (Phase 5)" @echo " gomobile gomobile bind for iOS .framework + Android .aar (Phase 32+)" @echo " desktop-mac Wails build for darwin/{arm64,amd64} (Phase 31)" @echo " desktop-win Wails build for windows/amd64 (Phase 31)" @@ -14,6 +19,17 @@ help: @echo " android Capacitor sync + gradle assembleRelease (Phase 32+)" @echo " all every target above" -web wasm gomobile desktop-mac desktop-win desktop-linux ios android all: +wasm: + @command -v tinygo >/dev/null || { echo "tinygo not found; install via 'brew install tinygo' (see ui/docs/wasm-toolchain.md)"; exit 1; } + tinygo build -o $(WASM_OUT) -target=wasm ./wasm + cp $(TINYGO_ROOT)/targets/wasm_exec.js $(WASM_EXEC) + @printf "core.wasm: %s\n" "$$(ls -lh $(WASM_OUT) | awk '{print $$5}')" + +ts-protos: + @command -v buf >/dev/null || { echo "buf not found; install via 'brew install bufbuild/buf/buf' or see https://buf.build/docs/installation"; exit 1; } + @test -x frontend/node_modules/.bin/protoc-gen-es || { echo "protoc-gen-es not installed; run 'pnpm install' inside ui/frontend"; exit 1; } + buf generate ../gateway --template buf.gen.yaml --include-imports + +web gomobile desktop-mac desktop-win desktop-linux ios android all: @echo "TODO: implement '$@' (placeholder, see ui/PLAN.md)" @exit 1 diff --git a/ui/PLAN.md b/ui/PLAN.md index a6cb15c..9de8834 100644 --- a/ui/PLAN.md +++ b/ui/PLAN.md @@ -506,51 +506,97 @@ add coverage. Future contributors looking for "the Connect tests" can read any file in `gateway/internal/grpcapi/` — they all use the Connect client now. -## Phase 5. WASM Build, `WasmCore` Adapter, `GalaxyClient` Skeleton +## ~~Phase 5. WASM Build, `WasmCore` Adapter, `GalaxyClient` Skeleton~~ -Status: pending. +Status: done. Goal: package `ui/core` as a WASM module, expose it to TypeScript through a typed adapter, and prove the WASM-side crypto pipeline at unit level. End-to-end Connect round-trip is validated in Phase 7 (authenticated calls only become possible after login). -Artifacts: +Decisions taken with the project owner before implementation: -- `ui/wasm/main.go` TinyGo entry point exporting `Core` API to JS -- `ui/Makefile` target `wasm` producing `core.wasm` and `wasm_exec.js` - under `ui/frontend/static/` -- `ui/frontend/src/platform/core/index.ts` `Core` interface and - build-time target resolver -- `ui/frontend/src/platform/core/wasm.ts` `WasmCore` adapter -- `ui/frontend/src/api/galaxy-client.ts` `GalaxyClient` orchestrating - `Core.signRequest` → ConnectRPC fetch → `Core.verifyResponse` -- `ui/frontend/src/api/connect.ts` typed Connect client built from - generated stubs (Connect codegen via `@bufbuild/protoc-gen-es` and - `@connectrpc/protoc-gen-connect-es`) -- topic doc `ui/docs/wasm-toolchain.md` documenting TinyGo vs - standard Go choice and bundle size measured +1. **TinyGo as primary toolchain.** `core.wasm` lands at 903 KB — + well under the 1 MB acceptance bar. The `GOOS=js GOARCH=wasm` + fallback path stays documented in `ui/docs/wasm-toolchain.md`. +2. **`Core.signRequest` returns canonical bytes only.** No private + key inside WASM; Phase 6 plugs WebCrypto's non-exportable keys at + the orchestration layer. `GalaxyClient` takes a pluggable `Signer` + so Phase 5 tests pass a fixture-key signer and Phase 6 swaps in + WebCrypto without touching the orchestration. +3. **TS codegen runs locally, not against buf.build BSR.** A new + `ui/buf.gen.yaml` invokes + `frontend/node_modules/.bin/protoc-gen-es` (added as a + devDependency). This sidesteps BSR rate limiting and removes the + network dependency from the codegen step. +4. **Field naming is camelCase end-to-end.** Both the TS `Core` + interface and the Go bridge in `ui/wasm/main.go` use camelCase + field names; there is no snake-case translation layer. + +Artifacts (delivered): + +- `ui/wasm/main.go` TinyGo entry point on `globalThis.galaxyCore` + with four functions: `signRequest`, `verifyResponse`, + `verifyEvent`, `verifyPayloadHash`. +- `ui/Makefile` `wasm` and `ts-protos` targets. +- `ui/buf.gen.yaml` with the local Protobuf-ES plugin (single plugin — + protobuf-es v2 emits both message types and Connect service + descriptors in one file). +- `ui/frontend/src/platform/core/index.ts` — typed `Core` interface + plus a `loadCore()` resolver (Phase 5 ships only the WASM adapter). +- `ui/frontend/src/platform/core/wasm.ts` — `WasmCore` adapter for + browsers; the JSDOM test path lives next to it in + `ui/frontend/tests/setup-wasm.ts`. +- `ui/frontend/src/api/connect.ts` — typed Connect-Web transport + + `EdgeGatewayClient` factory. +- `ui/frontend/src/api/galaxy-client.ts` — `GalaxyClient` skeleton + with injected `Signer` and `Sha256` dependencies. +- `ui/frontend/src/proto/galaxy/gateway/v1/edge_gateway_pb.ts` + (generated) and `ui/frontend/src/proto/buf/validate/validate_pb.ts` + (generated as a transitive import via `--include-imports`). +- `ui/frontend/static/core.wasm` (903 KB) + `wasm_exec.js` (TinyGo + shim). +- Three Vitest files exercising the bridge end-to-end: + `tests/wasm-core.test.ts` (each Core method, including a sanity + `signRequest` check that the canonical bytes start with the v1 + domain marker), `tests/wasm-core-canon-parity.test.ts` (byte-for- + byte parity against three request fixtures plus the response and + event signature fixtures from `ui/core/canon/testdata/`), and + `tests/galaxy-client.test.ts` (orchestration through a mock `Core` + and `createRouterTransport` from `@connectrpc/connect`). +- Topic doc `ui/docs/wasm-toolchain.md`. +- `ui/README.md` repository-layout block. Dependencies: Phases 2, 3, 4. -Acceptance criteria: +Acceptance criteria (met): -- `make wasm` produces a deterministic bundle under 1 MB (TinyGo) or - under 3 MB (standard Go fallback); +- `make wasm` produces `core.wasm` deterministically under 1 MB (903 + KB measured); - `WasmCore.signRequest` produces canonical bytes byte-for-byte - identical to the gateway-side verifier output on shared fixtures - (validated via Vitest with the WASM module loaded in JSDOM); -- `WasmCore` exposes the same TypeScript types as the future - `WailsCore` and `CapacitorCore` will need to satisfy. + identical to the gateway-side fixtures for three message types + (`request_user_account_get`, `request_user_games_command`, + `request_lobby_my_games_list`); +- `WasmCore` exposes the same `Core` TypeScript types future + `WailsCore` and `CapacitorCore` adapters will satisfy. -Targeted tests: +Targeted tests (delivered): -- Vitest unit tests for `WasmCore` calling each public method with a - fixture WASM module loaded in JSDOM; -- Vitest unit tests for `GalaxyClient` using a mock `Core` and a mock - Connect transport; -- Vitest tests asserting `WasmCore.signRequest` output matches gateway - fixtures byte-for-byte for at least three message types. +- Vitest unit tests for `WasmCore` calling each public method with + the WASM module loaded in JSDOM via `tests/setup-wasm.ts`; +- Vitest unit tests for `GalaxyClient` using a mock `Core` and the + in-memory `createRouterTransport`; +- Vitest tests asserting `WasmCore.signRequest` output matches the + committed gateway fixtures byte-for-byte for the three request + message types listed above. + +Decision deviation note: the initial plan listed `protoc-gen-es` and +`protoc-gen-connect-es` as separate plugins. Protobuf-ES v2 generates +service descriptors in the `_pb.ts` file directly, so a single +`@bufbuild/protoc-gen-es` plugin is sufficient — `@connectrpc/connect` +v2 consumes those descriptors via `createClient`. The `connect-es` +plugin is a v1-only path and is intentionally not used here. ## Phase 6. Storage Layer (Web) diff --git a/ui/README.md b/ui/README.md index 2114ae7..6ae6de8 100644 --- a/ui/README.md +++ b/ui/README.md @@ -56,7 +56,30 @@ under `ui/docs/` as they are introduced. ## Repository layout -Filled in incrementally as phases land. Today only `frontend/` exists. +```text +ui/ +├── PLAN.md staged implementation plan (Phases 1-36) +├── Makefile wasm / ts-protos / web / mobile / desktop targets +├── README.md this file +├── buf.gen.yaml local-plugin TS Protobuf-ES generator +├── docs/ topic-based design notes +│ ├── testing.md per-PR / release test tiers +│ └── wasm-toolchain.md TinyGo build, JSDOM loading, bundle budget +├── core/ ui/core Go module (canonical bytes, keypair) +├── wasm/ TinyGo entry point exposing Core to JS +└── frontend/ SvelteKit / Vite source + ├── src/api/ GalaxyClient + typed Connect client + ├── src/platform/core/ Core interface + WasmCore adapter + ├── src/proto/ generated Protobuf-ES + Connect descriptors + └── static/ core.wasm + wasm_exec.js (committed artefacts) +``` + +Linked topic docs: + +- [`docs/wasm-toolchain.md`](docs/wasm-toolchain.md) — TinyGo build, + loading recipe, bundle size budget. +- [`docs/testing.md`](docs/testing.md) — Tier 1 per-PR + Tier 2 + release test tiers. ```text ui/ diff --git a/ui/buf.gen.yaml b/ui/buf.gen.yaml new file mode 100644 index 0000000..215f997 --- /dev/null +++ b/ui/buf.gen.yaml @@ -0,0 +1,12 @@ +version: v2 + +# Generates the TypeScript Protobuf-ES + Connect-ES service descriptors +# from the gateway's authenticated edge .proto files into the SvelteKit +# frontend's source tree. The plugin runs locally from +# `frontend/node_modules/.bin/protoc-gen-es` (added as a devDependency +# in `frontend/package.json`) — no network call to buf.build BSR. +plugins: + - local: frontend/node_modules/.bin/protoc-gen-es + out: frontend/src/proto + opt: + - target=ts diff --git a/ui/docs/wasm-toolchain.md b/ui/docs/wasm-toolchain.md new file mode 100644 index 0000000..a6ea923 --- /dev/null +++ b/ui/docs/wasm-toolchain.md @@ -0,0 +1,110 @@ +# WASM Toolchain + +The Galaxy UI client compiles the Go module `ui/core` (canonical +bytes, signature verification, keypair helpers) to WebAssembly via +**TinyGo**. The compiled artefact `core.wasm` and its companion +runtime shim `wasm_exec.js` ship under `ui/frontend/static/`. + +## Why TinyGo + +Two viable Go-to-WASM toolchains exist: + +| Toolchain | Bundle size (Phase 5) | Notes | +|---------------|------------------------------------|--------------------------------------------| +| **TinyGo** | ~903 KB (under 1 MB acceptance bar) | LLVM-based, no full GC, fast cold-start | +| Standard Go | ~2 MB (`GOOS=js GOARCH=wasm`) | Drops in without extra tooling | + +`ui/core` was written under the TinyGo invariants documented in +`ui/core/README.md` (no `crypto/x509`, no `encoding/pem`, no +goroutines or `sync` primitives in production files), so the TinyGo +build is a drop-in compile. + +The standard-Go fallback stays available in case TinyGo lags behind a +future Go release we depend on. To switch the build, swap the +`tinygo build` invocation in `ui/Makefile` for +`GOOS=js GOARCH=wasm go build -o frontend/static/core.wasm ./wasm`, +and copy the matching shim from +`$(go env GOROOT)/lib/wasm/wasm_exec.js`. + +## Prerequisites + +- TinyGo ≥ 0.41 (`brew install tinygo`). +- Go 1.26+ (TinyGo's host compiler). +- `buf` 1.67+ on PATH for TS protobuf generation + (`brew install bufbuild/buf/buf`). +- pnpm + Node 22+ for the JS runtime. + +## Build commands + +```bash +make -C ui wasm # produces frontend/static/{core.wasm,wasm_exec.js} +make -C ui ts-protos # regenerates frontend/src/proto/* from gateway/proto +``` + +`make wasm` runs `tinygo build -target=wasm` and copies the matching +TinyGo shim into the static asset directory. The shim **must** be +the TinyGo one — the standard Go shim is ABI-incompatible. The +Makefile resolves the shim path via `tinygo env TINYGOROOT`. + +## Loading recipes + +### Browser + +SvelteKit serves `static/` at the application root, so the WASM +adapter at `ui/frontend/src/platform/core/wasm.ts` does the +following on first call: + +1. Inject `