ecd2bc9348
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>
115 lines
4.3 KiB
Markdown
115 lines
4.3 KiB
Markdown
# 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`](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).
|
|
- `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 `<script src="/wasm_exec.js">` if `globalThis.Go` is
|
|
undefined.
|
|
2. `fetch('/core.wasm')` and instantiate via
|
|
`WebAssembly.instantiate`.
|
|
3. Spawn the Go runtime via `go.run(instance)`.
|
|
4. Read `globalThis.galaxyCore` and wrap it as the typed `Core`
|
|
interface.
|
|
|
|
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.
|