146 lines
5.0 KiB
Go
146 lines
5.0 KiB
Go
// Command wasm is the TinyGo WebAssembly entry point for the Galaxy UI
|
|
// client. It exposes a small "compute boundary" API on
|
|
// `globalThis.galaxyCore` so the TypeScript-side `WasmCore` adapter can
|
|
// call into the Go canonical-bytes serializer and signature verifier
|
|
// without duplicating the contract in JavaScript.
|
|
//
|
|
// Public surface (all functions live under `globalThis.galaxyCore`):
|
|
//
|
|
// - signRequest(fields) -> Uint8Array
|
|
// Returns the canonical bytes for a v1 request envelope. The actual
|
|
// Ed25519 signing happens outside WASM (Phase 6 introduces WebCrypto
|
|
// with non-exportable keys).
|
|
// - verifyResponse(publicKey, signature, fields) -> boolean
|
|
// - verifyEvent(publicKey, signature, fields) -> boolean
|
|
// - verifyPayloadHash(payloadBytes, payloadHash) -> boolean
|
|
//
|
|
// Field objects are plain JS objects with camelCase keys matching the
|
|
// TypeScript `Core` interface, and bytes fields are Uint8Array.
|
|
// Timestamps are JS Number (Unix milliseconds fit in 53 bits well past
|
|
// year 2200).
|
|
//
|
|
// All functions return either a Uint8Array, a boolean, or fail closed.
|
|
// They never throw — callers may inspect the boolean result or rely on
|
|
// the canon-byte length to detect malformed input.
|
|
|
|
//go:build js && wasm
|
|
|
|
package main
|
|
|
|
import (
|
|
"syscall/js"
|
|
|
|
"galaxy/core/canon"
|
|
)
|
|
|
|
func main() {
|
|
js.Global().Set("galaxyCore", js.ValueOf(map[string]any{
|
|
"signRequest": js.FuncOf(signRequest),
|
|
"verifyResponse": js.FuncOf(verifyResponse),
|
|
"verifyEvent": js.FuncOf(verifyEvent),
|
|
"verifyPayloadHash": js.FuncOf(verifyPayloadHash),
|
|
}))
|
|
|
|
// Block forever so the Go runtime stays alive while JS keeps calling
|
|
// the registered functions.
|
|
select {}
|
|
}
|
|
|
|
func signRequest(_ js.Value, args []js.Value) any {
|
|
if len(args) != 1 {
|
|
return js.Null()
|
|
}
|
|
fields := canon.RequestSigningFields{
|
|
ProtocolVersion: args[0].Get("protocolVersion").String(),
|
|
DeviceSessionID: args[0].Get("deviceSessionId").String(),
|
|
MessageType: args[0].Get("messageType").String(),
|
|
TimestampMS: int64(args[0].Get("timestampMs").Float()),
|
|
RequestID: args[0].Get("requestId").String(),
|
|
PayloadHash: copyBytesFromJS(args[0].Get("payloadHash")),
|
|
}
|
|
return copyBytesToJS(canon.BuildRequestSigningInput(fields))
|
|
}
|
|
|
|
func verifyResponse(_ js.Value, args []js.Value) any {
|
|
if len(args) != 3 {
|
|
return js.ValueOf(false)
|
|
}
|
|
fields := canon.ResponseSigningFields{
|
|
ProtocolVersion: args[2].Get("protocolVersion").String(),
|
|
RequestID: args[2].Get("requestId").String(),
|
|
TimestampMS: int64(args[2].Get("timestampMs").Float()),
|
|
ResultCode: args[2].Get("resultCode").String(),
|
|
PayloadHash: copyBytesFromJS(args[2].Get("payloadHash")),
|
|
}
|
|
publicKey := copyBytesFromJS(args[0])
|
|
signature := copyBytesFromJS(args[1])
|
|
if err := canon.VerifyResponseSignature(publicKey, signature, fields); err != nil {
|
|
return js.ValueOf(false)
|
|
}
|
|
return js.ValueOf(true)
|
|
}
|
|
|
|
func verifyEvent(_ js.Value, args []js.Value) any {
|
|
if len(args) != 3 {
|
|
return js.ValueOf(false)
|
|
}
|
|
fields := canon.EventSigningFields{
|
|
EventType: args[2].Get("eventType").String(),
|
|
EventID: args[2].Get("eventId").String(),
|
|
TimestampMS: int64(args[2].Get("timestampMs").Float()),
|
|
RequestID: args[2].Get("requestId").String(),
|
|
TraceID: args[2].Get("traceId").String(),
|
|
PayloadHash: copyBytesFromJS(args[2].Get("payloadHash")),
|
|
}
|
|
publicKey := copyBytesFromJS(args[0])
|
|
signature := copyBytesFromJS(args[1])
|
|
if err := canon.VerifyEventSignature(publicKey, signature, fields); err != nil {
|
|
return js.ValueOf(false)
|
|
}
|
|
return js.ValueOf(true)
|
|
}
|
|
|
|
func verifyPayloadHash(_ js.Value, args []js.Value) any {
|
|
if len(args) != 2 {
|
|
return js.ValueOf(false)
|
|
}
|
|
payloadBytes := copyBytesFromJS(args[0])
|
|
payloadHash := copyBytesFromJS(args[1])
|
|
if err := canon.VerifyPayloadHash(payloadBytes, payloadHash); err != nil {
|
|
return js.ValueOf(false)
|
|
}
|
|
return js.ValueOf(true)
|
|
}
|
|
|
|
// copyBytesFromJS materialises a JS Uint8Array (or any indexable
|
|
// byte-shaped value) into a Go byte slice. We avoid `js.CopyBytesToGo`
|
|
// because TinyGo's implementation panics on values it does not
|
|
// recognise as Uint8Array — a check that misfires for Uint8Arrays
|
|
// constructed from Node's Buffer in Vitest's jsdom environment. The
|
|
// per-element copy is slower but always correct, and the canonical
|
|
// envelope payloads are small enough (≤ a few hundred bytes) that the
|
|
// difference is negligible.
|
|
func copyBytesFromJS(value js.Value) []byte {
|
|
if value.IsUndefined() || value.IsNull() {
|
|
return nil
|
|
}
|
|
length := value.Length()
|
|
if length == 0 {
|
|
return nil
|
|
}
|
|
dst := make([]byte, length)
|
|
for i := 0; i < length; i++ {
|
|
dst[i] = byte(value.Index(i).Int())
|
|
}
|
|
return dst
|
|
}
|
|
|
|
// copyBytesToJS allocates a JS Uint8Array of the same length as src and
|
|
// copies src into it. The result is safe to hand back across the
|
|
// JS/Go boundary as the canonical-bytes return value.
|
|
func copyBytesToJS(src []byte) js.Value {
|
|
dst := js.Global().Get("Uint8Array").New(len(src))
|
|
js.CopyBytesToJS(dst, src)
|
|
return dst
|
|
}
|