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,211 @@
|
||||
// Verifies the orchestration order in `GalaxyClient.executeCommand`:
|
||||
//
|
||||
// core.signRequest(fields)
|
||||
// → signer(canonicalBytes)
|
||||
// → edge.executeCommand({ envelope, signature })
|
||||
// → core.verifyPayloadHash(response payload, hash)
|
||||
// → core.verifyResponse(envelope, signature)
|
||||
// → return response.payloadBytes
|
||||
//
|
||||
// Tests use a hand-rolled mock `Core` and a Connect router transport
|
||||
// from `@connectrpc/connect`, which lets us assert what the gateway
|
||||
// would have received without standing up a real Connect server.
|
||||
|
||||
import { create } from "@bufbuild/protobuf";
|
||||
import { createClient, createRouterTransport } from "@connectrpc/connect";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { GalaxyClient } from "../src/api/galaxy-client";
|
||||
import {
|
||||
EdgeGateway,
|
||||
ExecuteCommandResponseSchema,
|
||||
type ExecuteCommandRequest,
|
||||
} from "../src/proto/galaxy/gateway/v1/edge_gateway_pb";
|
||||
import type {
|
||||
Core,
|
||||
RequestSigningFields,
|
||||
ResponseSigningFields,
|
||||
} from "../src/platform/core/index";
|
||||
|
||||
const FIXED_REQUEST_ID = "req-test-1";
|
||||
const FIXED_TIMESTAMP = 1_700_000_000_000n;
|
||||
|
||||
describe("GalaxyClient.executeCommand", () => {
|
||||
test("orchestrates sign → fetch → verify and returns the payload", async () => {
|
||||
const canonicalBytes = new Uint8Array([0xca, 0xfe]);
|
||||
const signature = new Uint8Array(64).fill(0x55);
|
||||
const responsePayload = new TextEncoder().encode("hello-from-server");
|
||||
const responseHash = new Uint8Array(32).fill(0x77);
|
||||
const responseSignature = new Uint8Array(64).fill(0x99);
|
||||
const responsePublicKey = new Uint8Array(32).fill(0x11);
|
||||
|
||||
const core = mockCore({
|
||||
signRequestImpl: () => canonicalBytes,
|
||||
verifyResponseImpl: () => true,
|
||||
verifyPayloadHashImpl: () => true,
|
||||
});
|
||||
const signer = vi.fn(async () => signature);
|
||||
const sha256 = vi.fn(async () => new Uint8Array(32).fill(0x33));
|
||||
let captured: ExecuteCommandRequest | undefined;
|
||||
const transport = createRouterTransport(({ service }) => {
|
||||
service(EdgeGateway, {
|
||||
executeCommand(req) {
|
||||
captured = req;
|
||||
return create(ExecuteCommandResponseSchema, {
|
||||
protocolVersion: "v1",
|
||||
requestId: FIXED_REQUEST_ID,
|
||||
timestampMs: FIXED_TIMESTAMP,
|
||||
resultCode: "ok",
|
||||
payloadBytes: responsePayload,
|
||||
payloadHash: responseHash,
|
||||
signature: responseSignature,
|
||||
});
|
||||
},
|
||||
subscribeEvents() {
|
||||
throw new Error("not used in this test");
|
||||
},
|
||||
});
|
||||
});
|
||||
const edge = createClient(EdgeGateway, transport);
|
||||
|
||||
const client = new GalaxyClient({
|
||||
core,
|
||||
edge,
|
||||
signer,
|
||||
sha256,
|
||||
deviceSessionId: "device-session-1",
|
||||
gatewayResponsePublicKey: responsePublicKey,
|
||||
clock: () => FIXED_TIMESTAMP,
|
||||
requestIdFactory: () => FIXED_REQUEST_ID,
|
||||
});
|
||||
|
||||
const out = await client.executeCommand(
|
||||
"user.account.get",
|
||||
new TextEncoder().encode("client-payload"),
|
||||
);
|
||||
|
||||
expect(Array.from(out)).toEqual(Array.from(responsePayload));
|
||||
expect(signer).toHaveBeenCalledWith(canonicalBytes);
|
||||
expect(sha256).toHaveBeenCalledTimes(1);
|
||||
expect(core.signRequest).toHaveBeenCalledTimes(1);
|
||||
const verifyHashCall = vi.mocked(core.verifyPayloadHash).mock.calls[0]!;
|
||||
expect(Array.from(verifyHashCall[0])).toEqual(Array.from(responsePayload));
|
||||
expect(Array.from(verifyHashCall[1])).toEqual(Array.from(responseHash));
|
||||
const verifyRespCall = vi.mocked(core.verifyResponse).mock.calls[0]!;
|
||||
expect(Array.from(verifyRespCall[0])).toEqual(Array.from(responsePublicKey));
|
||||
expect(Array.from(verifyRespCall[1])).toEqual(Array.from(responseSignature));
|
||||
expect(verifyRespCall[2]).toMatchObject({
|
||||
protocolVersion: "v1",
|
||||
requestId: FIXED_REQUEST_ID,
|
||||
resultCode: "ok",
|
||||
});
|
||||
expect(captured?.deviceSessionId).toBe("device-session-1");
|
||||
expect(captured?.messageType).toBe("user.account.get");
|
||||
expect(captured?.requestId).toBe(FIXED_REQUEST_ID);
|
||||
expect(Array.from(captured?.signature ?? [])).toEqual(
|
||||
Array.from(signature),
|
||||
);
|
||||
});
|
||||
|
||||
test("throws when the response signature fails verification", async () => {
|
||||
const core = mockCore({
|
||||
signRequestImpl: () => new Uint8Array(),
|
||||
verifyResponseImpl: () => false,
|
||||
verifyPayloadHashImpl: () => true,
|
||||
});
|
||||
const transport = createRouterTransport(({ service }) => {
|
||||
service(EdgeGateway, {
|
||||
executeCommand: () =>
|
||||
create(ExecuteCommandResponseSchema, {
|
||||
protocolVersion: "v1",
|
||||
requestId: FIXED_REQUEST_ID,
|
||||
timestampMs: FIXED_TIMESTAMP,
|
||||
resultCode: "ok",
|
||||
payloadBytes: new Uint8Array(),
|
||||
payloadHash: new Uint8Array(32),
|
||||
signature: new Uint8Array(64),
|
||||
}),
|
||||
subscribeEvents() {
|
||||
throw new Error("not used in this test");
|
||||
},
|
||||
});
|
||||
});
|
||||
const client = new GalaxyClient({
|
||||
core,
|
||||
edge: createClient(EdgeGateway, transport),
|
||||
signer: async () => new Uint8Array(64),
|
||||
sha256: async () => new Uint8Array(32),
|
||||
deviceSessionId: "device-session-1",
|
||||
gatewayResponsePublicKey: new Uint8Array(32),
|
||||
clock: () => FIXED_TIMESTAMP,
|
||||
requestIdFactory: () => FIXED_REQUEST_ID,
|
||||
});
|
||||
await expect(
|
||||
client.executeCommand("user.account.get", new Uint8Array()),
|
||||
).rejects.toThrow(/invalid response signature/);
|
||||
});
|
||||
|
||||
test("throws when the response payload_hash does not match", async () => {
|
||||
const core = mockCore({
|
||||
signRequestImpl: () => new Uint8Array(),
|
||||
verifyResponseImpl: () => true,
|
||||
verifyPayloadHashImpl: () => false,
|
||||
});
|
||||
const transport = createRouterTransport(({ service }) => {
|
||||
service(EdgeGateway, {
|
||||
executeCommand: () =>
|
||||
create(ExecuteCommandResponseSchema, {
|
||||
protocolVersion: "v1",
|
||||
requestId: FIXED_REQUEST_ID,
|
||||
timestampMs: FIXED_TIMESTAMP,
|
||||
resultCode: "ok",
|
||||
payloadBytes: new Uint8Array(),
|
||||
payloadHash: new Uint8Array(32),
|
||||
signature: new Uint8Array(64),
|
||||
}),
|
||||
subscribeEvents() {
|
||||
throw new Error("not used in this test");
|
||||
},
|
||||
});
|
||||
});
|
||||
const client = new GalaxyClient({
|
||||
core,
|
||||
edge: createClient(EdgeGateway, transport),
|
||||
signer: async () => new Uint8Array(64),
|
||||
sha256: async () => new Uint8Array(32),
|
||||
deviceSessionId: "device-session-1",
|
||||
gatewayResponsePublicKey: new Uint8Array(32),
|
||||
clock: () => FIXED_TIMESTAMP,
|
||||
requestIdFactory: () => FIXED_REQUEST_ID,
|
||||
});
|
||||
await expect(
|
||||
client.executeCommand("user.account.get", new Uint8Array()),
|
||||
).rejects.toThrow(/payload_hash mismatch/);
|
||||
});
|
||||
});
|
||||
|
||||
interface MockCoreOptions {
|
||||
signRequestImpl: (fields: RequestSigningFields) => Uint8Array;
|
||||
verifyResponseImpl: (
|
||||
publicKey: Uint8Array,
|
||||
signature: Uint8Array,
|
||||
fields: ResponseSigningFields,
|
||||
) => boolean;
|
||||
verifyPayloadHashImpl: (
|
||||
payload: Uint8Array,
|
||||
hash: Uint8Array,
|
||||
) => boolean;
|
||||
}
|
||||
|
||||
function mockCore(opts: MockCoreOptions): Core & {
|
||||
signRequest: ReturnType<typeof vi.fn>;
|
||||
verifyResponse: ReturnType<typeof vi.fn>;
|
||||
verifyPayloadHash: ReturnType<typeof vi.fn>;
|
||||
verifyEvent: ReturnType<typeof vi.fn>;
|
||||
} {
|
||||
return {
|
||||
signRequest: vi.fn(opts.signRequestImpl),
|
||||
verifyResponse: vi.fn(opts.verifyResponseImpl),
|
||||
verifyEvent: vi.fn(() => true),
|
||||
verifyPayloadHash: vi.fn(opts.verifyPayloadHashImpl),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Boots the TinyGo `core.wasm` module under Vitest/JSDOM by reading
|
||||
// the artefact and the matching wasm_exec.js shim from disk, evaluating
|
||||
// the shim into the test's global scope, then instantiating WebAssembly
|
||||
// against `(new Go()).importObject`. Returns a `Core` ready to use in
|
||||
// tests; subsequent calls return the cached instance so each Vitest
|
||||
// file pays the boot cost once.
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { runInThisContext } from "node:vm";
|
||||
import { adaptBridge, requireBridge } from "../src/platform/core/wasm";
|
||||
import type { Core } from "../src/platform/core/index";
|
||||
|
||||
// Vitest is launched from ui/frontend, so the static artefacts produced
|
||||
// by `make wasm` sit at <cwd>/static/. Resolving via process.cwd avoids
|
||||
// the JSDOM-specific "URL must be of scheme file" error that
|
||||
// import.meta.url triggers under the jsdom test environment.
|
||||
const STATIC_DIR = resolve(process.cwd(), "static") + "/";
|
||||
|
||||
let cached: Promise<Core> | undefined;
|
||||
|
||||
export function loadWasmCoreForTest(): Promise<Core> {
|
||||
if (!cached) {
|
||||
cached = boot();
|
||||
}
|
||||
return cached;
|
||||
}
|
||||
|
||||
async function boot(): Promise<Core> {
|
||||
if (typeof globalThis.Go === "undefined") {
|
||||
const shim = readFileSync(`${STATIC_DIR}wasm_exec.js`, "utf8");
|
||||
runInThisContext(shim);
|
||||
}
|
||||
const Go = globalThis.Go;
|
||||
if (!Go) {
|
||||
throw new Error("setup-wasm: Go runtime missing after wasm_exec.js eval");
|
||||
}
|
||||
const go = new Go();
|
||||
const wasm = readFileSync(`${STATIC_DIR}core.wasm`);
|
||||
const { instance } = await WebAssembly.instantiate(
|
||||
wasm,
|
||||
go.importObject,
|
||||
);
|
||||
void go.run(instance);
|
||||
// `go.run` is async but the Go side registers `globalThis.galaxyCore`
|
||||
// and then blocks on `select {}`; the registration happens
|
||||
// synchronously before the first await, so by the time the next
|
||||
// microtask runs the bridge is already available.
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, 0));
|
||||
return adaptBridge(requireBridge());
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
// Asserts that the WASM-compiled `ui/core` produces canonical bytes
|
||||
// byte-for-byte identical to the gateway-side fixtures committed in
|
||||
// `ui/core/canon/testdata/`. The fixtures themselves are exercised by
|
||||
// the Go-side parity test in `gateway/authn/parity_with_ui_core_test.go`,
|
||||
// so passing both proves Go-Go parity AND Go-TS parity through WASM.
|
||||
//
|
||||
// The signature parity check additionally proves that a signature
|
||||
// produced by Node's `ed25519` over the WASM-built canonical bytes is
|
||||
// accepted by the gateway's `VerifyResponseSignature` /
|
||||
// `VerifyEventSignature` (re-checked here via Core.verifyResponse /
|
||||
// Core.verifyEvent against the same fixture key).
|
||||
|
||||
import {
|
||||
createPrivateKey,
|
||||
createPublicKey,
|
||||
sign as cryptoSign,
|
||||
} from "node:crypto";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { beforeAll, describe, expect, test } from "vitest";
|
||||
import type { Core } from "../src/platform/core/index";
|
||||
import { loadWasmCoreForTest } from "./setup-wasm";
|
||||
|
||||
const TESTDATA_DIR = resolve(
|
||||
process.cwd(),
|
||||
"..",
|
||||
"core",
|
||||
"canon",
|
||||
"testdata",
|
||||
) + "/";
|
||||
|
||||
interface RequestFixture {
|
||||
protocol_version: string;
|
||||
device_session_id: string;
|
||||
message_type: string;
|
||||
timestamp_ms: number;
|
||||
request_id: string;
|
||||
payload: string;
|
||||
payload_hash_hex: string;
|
||||
expected_canonical_bytes_hex: string;
|
||||
private_key_seed_hex: string;
|
||||
public_key_base64: string;
|
||||
expected_signature_hex: string;
|
||||
}
|
||||
|
||||
interface ResponseFixture {
|
||||
protocol_version: string;
|
||||
request_id: string;
|
||||
timestamp_ms: number;
|
||||
result_code: string;
|
||||
payload: string;
|
||||
payload_hash_hex: string;
|
||||
expected_canonical_bytes_hex: string;
|
||||
private_key_seed_hex: string;
|
||||
public_key_base64: string;
|
||||
expected_signature_hex: string;
|
||||
}
|
||||
|
||||
interface EventFixture {
|
||||
event_type: string;
|
||||
event_id: string;
|
||||
timestamp_ms: number;
|
||||
request_id: string;
|
||||
trace_id: string;
|
||||
payload: string;
|
||||
payload_hash_hex: string;
|
||||
expected_canonical_bytes_hex: string;
|
||||
private_key_seed_hex: string;
|
||||
public_key_base64: string;
|
||||
expected_signature_hex: string;
|
||||
}
|
||||
|
||||
let core: Core;
|
||||
|
||||
beforeAll(async () => {
|
||||
core = await loadWasmCoreForTest();
|
||||
});
|
||||
|
||||
describe("WasmCore canon parity with gateway fixtures", () => {
|
||||
test.each([
|
||||
"request_user_account_get.json",
|
||||
"request_user_games_command.json",
|
||||
"request_lobby_my_games_list.json",
|
||||
])("%s — canonical bytes byte-for-byte equal", (name) => {
|
||||
const fixture = readJson<RequestFixture>(name);
|
||||
const canonical = core.signRequest({
|
||||
protocolVersion: fixture.protocol_version,
|
||||
deviceSessionId: fixture.device_session_id,
|
||||
messageType: fixture.message_type,
|
||||
timestampMs: BigInt(fixture.timestamp_ms),
|
||||
requestId: fixture.request_id,
|
||||
payloadHash: hexToBytes(fixture.payload_hash_hex),
|
||||
});
|
||||
expect(bytesToHex(canonical)).toBe(fixture.expected_canonical_bytes_hex);
|
||||
|
||||
const signature = signEd25519(
|
||||
hexToBytes(fixture.private_key_seed_hex),
|
||||
canonical,
|
||||
);
|
||||
expect(bytesToHex(signature)).toBe(fixture.expected_signature_hex);
|
||||
});
|
||||
|
||||
test("response_ok.json — verifyResponse accepts canonical signature", () => {
|
||||
const fixture = readJson<ResponseFixture>("response_ok.json");
|
||||
const publicKey = base64ToBytes(fixture.public_key_base64);
|
||||
const signature = hexToBytes(fixture.expected_signature_hex);
|
||||
const fields = {
|
||||
protocolVersion: fixture.protocol_version,
|
||||
requestId: fixture.request_id,
|
||||
timestampMs: BigInt(fixture.timestamp_ms),
|
||||
resultCode: fixture.result_code,
|
||||
payloadHash: hexToBytes(fixture.payload_hash_hex),
|
||||
};
|
||||
expect(core.verifyResponse(publicKey, signature, fields)).toBe(true);
|
||||
|
||||
const tampered = new Uint8Array(signature);
|
||||
tampered[0] ^= 0xff;
|
||||
expect(core.verifyResponse(publicKey, tampered, fields)).toBe(false);
|
||||
});
|
||||
|
||||
test("event_gateway_server_time.json — verifyEvent accepts canonical signature", () => {
|
||||
const fixture = readJson<EventFixture>("event_gateway_server_time.json");
|
||||
const publicKey = base64ToBytes(fixture.public_key_base64);
|
||||
const signature = hexToBytes(fixture.expected_signature_hex);
|
||||
const fields = {
|
||||
eventType: fixture.event_type,
|
||||
eventId: fixture.event_id,
|
||||
timestampMs: BigInt(fixture.timestamp_ms),
|
||||
requestId: fixture.request_id,
|
||||
traceId: fixture.trace_id,
|
||||
payloadHash: hexToBytes(fixture.payload_hash_hex),
|
||||
};
|
||||
expect(core.verifyEvent(publicKey, signature, fields)).toBe(true);
|
||||
|
||||
const tampered = new Uint8Array(signature);
|
||||
tampered[0] ^= 0xff;
|
||||
expect(core.verifyEvent(publicKey, tampered, fields)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
function readJson<T>(name: string): T {
|
||||
return JSON.parse(readFileSync(`${TESTDATA_DIR}${name}`, "utf8")) as T;
|
||||
}
|
||||
|
||||
function hexToBytes(value: string): Uint8Array {
|
||||
const length = value.length / 2;
|
||||
const out = new Uint8Array(length);
|
||||
for (let i = 0; i < length; i++) {
|
||||
out[i] = parseInt(value.substr(i * 2, 2), 16);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function bytesToHex(value: Uint8Array): string {
|
||||
return Array.from(value, (byte) =>
|
||||
byte.toString(16).padStart(2, "0"),
|
||||
).join("");
|
||||
}
|
||||
|
||||
function base64ToBytes(value: string): Uint8Array {
|
||||
return Uint8Array.from(Buffer.from(value, "base64"));
|
||||
}
|
||||
|
||||
// Node's `ed25519` accepts a raw 32-byte seed packaged as a PKCS#8 DER
|
||||
// blob; this helper synthesises that blob so the test fixture seed
|
||||
// matches the Go-side `ed25519.NewKeyFromSeed` shape used by the
|
||||
// committed canon golden values.
|
||||
function signEd25519(seed: Uint8Array, message: Uint8Array): Uint8Array {
|
||||
const pkcs8 = pkcs8FromEd25519Seed(seed);
|
||||
const key = createPrivateKey({ key: pkcs8, format: "der", type: "pkcs8" });
|
||||
const signature = cryptoSign(null, message, key);
|
||||
return new Uint8Array(signature);
|
||||
}
|
||||
|
||||
function pkcs8FromEd25519Seed(seed: Uint8Array): Buffer {
|
||||
if (seed.length !== 32) {
|
||||
throw new Error(`ed25519 seed must be 32 bytes, got ${seed.length}`);
|
||||
}
|
||||
// PKCS#8 wrapping for an Ed25519 raw seed:
|
||||
// SEQUENCE {
|
||||
// INTEGER 0,
|
||||
// SEQUENCE { OID 1.3.101.112 (Ed25519) },
|
||||
// OCTET STRING { OCTET STRING { seed } }
|
||||
// }
|
||||
const prefix = Buffer.from([
|
||||
0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70,
|
||||
0x04, 0x22, 0x04, 0x20,
|
||||
]);
|
||||
return Buffer.concat([prefix, Buffer.from(seed)]);
|
||||
}
|
||||
|
||||
// `createPublicKey` is imported only so future tests can derive the
|
||||
// public key from the same seed without going through the WASM bridge.
|
||||
// Suppress the unused-import warning by referencing the binding here.
|
||||
void createPublicKey;
|
||||
@@ -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