KeyStore + Cache TS interfaces with WebCrypto non-extractable Ed25519 keys persisted via IndexedDB (idb), plus thin api/session.ts that loads or creates the device session at app startup. Vitest unit tests under fake-indexeddb cover both adapters; Playwright e2e verifies the keypair survives reload and produces signatures still verifiable under the persisted public key (gateway round-trip moves to Phase 7's existing acceptance bullet). Browser baseline: WebCrypto Ed25519 — Chrome >=137, Firefox >=130, Safari >=17.4. No JS fallback; ui/docs/storage.md documents the matrix and the WebKit non-determinism quirk. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
4.3 KiB
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/.
Real Ed25519 signing happens outside WASM in a platform-specific
keystore — see storage.md for the web implementation
(WebCrypto non-extractable keys + IndexedDB).
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).
buf1.67+ on PATH for TS protobuf generation (brew install bufbuild/buf/buf).- pnpm + Node 22+ for the JS runtime.
Build commands
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:
- Inject
<script src="/wasm_exec.js">ifglobalThis.Gois undefined. fetch('/core.wasm')and instantiate viaWebAssembly.instantiate.- Spawn the Go runtime via
go.run(instance). - Read
globalThis.galaxyCoreand wrap it as the typedCoreinterface.
The module is cached for the life of the page; subsequent
loadCore() calls reuse the same Core instance.
Vitest under JSDOM
JSDOM does not implement fetch against file:// URLs, so the
test loader at ui/frontend/tests/setup-wasm.ts reads both files
from disk and evaluates wasm_exec.js via Node's
vm.runInThisContext. The result is the same Core shape; the
JS-side bridging is identical.
Field-name convention
The TS Core interface uses camelCase keys (matching the rest
of the SvelteKit / TS-side conventions). The Go bridge in
ui/wasm/main.go reads the same camelCase keys (protocolVersion,
deviceSessionId, etc.) directly from the JS objects via
syscall/js. There is no snake-case translation layer.
Byte marshalling
The bridge avoids js.CopyBytesToGo and instead does an explicit
per-element copy via value.Index(i).Int(). TinyGo 0.41's
js.CopyBytesToGo panics on Uint8Array instances that originate
from Node's Buffer (which surfaces in Vitest's JSDOM environment
when fixtures are read with crypto.createHash). The per-element
loop is slower in theory but irrelevant in practice: the canonical
envelope payloads stay below a few hundred bytes.
Reproducibility
TinyGo builds are not bit-for-bit deterministic (the binary embeds
build-machine identifiers). Treat the committed core.wasm as a
snapshot rebuilt by make wasm whenever ui/core/ or
ui/wasm/main.go changes. CI rebuilds the artefact from source for
its own asserts; the committed copy keeps Vitest from depending on
TinyGo being installed in every environment.
Bundle size
| Build | Date | Size |
|---|---|---|
| Phase 5 land | 2026-05-07 | 903 KB |
If the artefact ever crosses the 1 MB target, profile via
tinygo build -size full and trim before committing.