phase 5: wasm core, GalaxyClient skeleton, Connect-Web stubs
Compile `ui/core` to WebAssembly via TinyGo (903 KB) and expose four canonical-bytes / signature-verification functions on `globalThis.galaxyCore` from `ui/wasm/main.go`. The TypeScript-side `Core` interface plus a `WasmCore` adapter (browser + JSDOM loader) bridge those into a typed shape, and a `GalaxyClient` skeleton wires `Core.signRequest` → injected `Signer` → typed Connect client → `Core.verifyPayloadHash` / `verifyResponse`. Wire `ui/buf.gen.yaml` against the local `@bufbuild/protoc-gen-es` v2 binary (devDependency) so the codegen step does not depend on the buf.build BSR. Vitest covers the bridge end-to-end: per-method WasmCore tests under JSDOM, byte-for-byte canon parity against the gateway fixtures committed in Phase 3, and a `GalaxyClient` orchestration test using `createRouterTransport`. The committed `core.wasm` snapshot tracks TinyGo output so contributors run `make wasm` only when `ui/core/` changes; CI consumes the snapshot directly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { beforeAll, describe, expect, test } from "vitest";
|
||||
import type { Core } from "../src/platform/core/index";
|
||||
import { loadWasmCoreForTest } from "./setup-wasm";
|
||||
|
||||
let core: Core;
|
||||
|
||||
beforeAll(async () => {
|
||||
core = await loadWasmCoreForTest();
|
||||
});
|
||||
|
||||
describe("WasmCore.signRequest", () => {
|
||||
test("returns canonical bytes that lead with the v1 domain marker", () => {
|
||||
const fields = {
|
||||
protocolVersion: "v1",
|
||||
deviceSessionId: "device-session-1",
|
||||
messageType: "user.account.get",
|
||||
timestampMs: 1700000000000n,
|
||||
requestId: "req-1",
|
||||
payloadHash: sha256("payload"),
|
||||
};
|
||||
const canonical = core.signRequest(fields);
|
||||
const marker = "galaxy-request-v1";
|
||||
expect(canonical.length).toBeGreaterThan(marker.length);
|
||||
expect(new TextDecoder().decode(canonical.slice(1, 1 + marker.length))).toBe(
|
||||
marker,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("WasmCore.verifyResponse", () => {
|
||||
test("rejects a flipped-bit signature", () => {
|
||||
const publicKey = new Uint8Array(32);
|
||||
const signature = new Uint8Array(64);
|
||||
const fields = {
|
||||
protocolVersion: "v1",
|
||||
requestId: "req-1",
|
||||
timestampMs: 1700000000000n,
|
||||
resultCode: "ok",
|
||||
payloadHash: sha256("payload"),
|
||||
};
|
||||
expect(core.verifyResponse(publicKey, signature, fields)).toBe(false);
|
||||
});
|
||||
|
||||
test("returns false for malformed key/signature lengths", () => {
|
||||
const fields = {
|
||||
protocolVersion: "v1",
|
||||
requestId: "req-1",
|
||||
timestampMs: 1700000000000n,
|
||||
resultCode: "ok",
|
||||
payloadHash: sha256("payload"),
|
||||
};
|
||||
expect(
|
||||
core.verifyResponse(new Uint8Array(8), new Uint8Array(64), fields),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("WasmCore.verifyEvent", () => {
|
||||
test("rejects an empty signature", () => {
|
||||
const fields = {
|
||||
eventType: "gateway.server_time",
|
||||
eventId: "evt-1",
|
||||
timestampMs: 1700000000000n,
|
||||
requestId: "req-1",
|
||||
traceId: "trace-1",
|
||||
payloadHash: sha256("event"),
|
||||
};
|
||||
expect(
|
||||
core.verifyEvent(new Uint8Array(32), new Uint8Array(0), fields),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("WasmCore.verifyPayloadHash", () => {
|
||||
test("accepts the SHA-256 of the supplied payload", () => {
|
||||
const payload = new TextEncoder().encode("hello");
|
||||
expect(core.verifyPayloadHash(payload, sha256("hello"))).toBe(true);
|
||||
});
|
||||
|
||||
test("rejects a digest of unrelated bytes", () => {
|
||||
const payload = new TextEncoder().encode("hello");
|
||||
expect(core.verifyPayloadHash(payload, sha256("world"))).toBe(false);
|
||||
});
|
||||
|
||||
test("rejects a payload_hash whose length is not 32", () => {
|
||||
const payload = new TextEncoder().encode("hello");
|
||||
expect(core.verifyPayloadHash(payload, new Uint8Array(16))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
function sha256(value: string): Uint8Array {
|
||||
return new Uint8Array(createHash("sha256").update(value).digest());
|
||||
}
|
||||
Reference in New Issue
Block a user