Eve browser sample reads its payloads through definitions, and the verifier type-checks the page

main.ts called decodeCultNetPayload with no definition, which the required-definition signature refuses; esbuild strips
types, so only the browser noticed. The sample now holds a counter definition and plain-map definitions for the Eve
documents, and verify-eve-browser-network.mjs runs tsc over main.ts against the module map the bundle uses (and builds
cultcache-ts, which cultmesh-browser now imports) before bundling. The receipt is read as the typed wire object instead
of a cast of the Eve receipt type.

All commits build.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-11 20:37:29 +02:00
commit 195c37b867
2 changed files with 56 additions and 15 deletions

View file

@ -6,10 +6,29 @@ import {
decodeCultNetOperationPayload,
decodeCultNetPayload,
} from "cultmesh-browser";
import { defineDocumentType } from "@gamecult/cultcache-ts/document";
// A payload is read through the definition that says how its bytes are laid out. These are plain-map definitions: the
// provider's CultCache types are integer-keyed MessagePack, and the Eve documents are validated by their own parsers.
// CounterState is [counterId, count, receipts].
const counterDocument = defineDocumentType({
type: "sample.counter_state",
schemaId: "sample.counter_state.v1",
schema: {
parse: (value: unknown): [string, number, unknown] => {
if (!Array.isArray(value) || typeof value[0] !== "string" || typeof value[1] !== "number") {
throw new Error("The counter state document is not [counterId, count, receipts].");
}
return [value[0], value[1], value[2]];
},
},
});
const eveDocument = (type: string) => defineDocumentType({ type, schemaId: `${type}.v1`, schema: { parse: (value: unknown) => value } });
const surfaceDocument = eveDocument("gamecult.eve.surface");
const advertisementDocument = eveDocument("gamecult.eve.provider_advertisement");
// CounterState is an integer-keyed MessagePack object: [counterId, count, receipts].
const counterCount = (record: Parameters<typeof decodeCultNetPayload>[0]) =>
decodeCultNetPayload<[string, number, unknown]>(record)[1];
decodeCultNetPayload(record, counterDocument)[1];
declare global {
interface Window {
@ -63,8 +82,12 @@ try {
recordKey: "sample.counter-ui",
subscriptionId: "browser-provider",
});
const provider = parseEveProviderAdvertisement(decodeCultNetPayload(advertisement.current!));
const currentSurface = parseEveSurfaceDocument(decodeCultNetPayload(surface.current!));
// Eve's generated contract type and the lowering's own interface disagree on optional plugin requirements; the
// value is the one parseEveProviderAdvertisement validated.
const provider = parseEveProviderAdvertisement(
decodeCultNetPayload(advertisement.current!, advertisementDocument),
) as unknown as NonNullable<Parameters<typeof renderEveSurface>[2]>["provider"];
const currentSurface = parseEveSurfaceDocument(decodeCultNetPayload(surface.current!, surfaceDocument));
renderEveSurface(currentSurface, host, {
activeSurfaceId: "sample.counter",
clientId: "sample.chromium",
@ -77,10 +100,8 @@ try {
payload: { amount: 1 },
idempotencyKey: window.__sampleCommandId,
});
const receipt = parseEveCommandReceipt(decodeCultNetOperationPayload(response)) as {
receiptId: string;
count: number;
};
const receipt = decodeCultNetOperationPayload<{ receiptId: string; count: number }>(response);
parseEveCommandReceipt(receipt);
window.__sampleReceipt = receipt;
receiptOutput.textContent = receipt.receiptId;
},

View file

@ -2,7 +2,7 @@ import assert from "node:assert/strict";
import { spawn } from "node:child_process";
import { existsSync } from "node:fs";
import { createServer as createHttpServer } from "node:http";
import { mkdtemp, readFile, rm } from "node:fs/promises";
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { createServer as createNetServer } from "node:net";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
@ -38,8 +38,33 @@ let browser;
try {
const tsc = join(repoRoot, "node_modules", "typescript", "bin", "tsc");
await run(process.execPath, [tsc, "-p", join(repoRoot, "packages", "cultcache-ts", "tsconfig.json"), "--pretty", "false"]);
await run(process.execPath, [tsc, "-p", join(repoRoot, "packages", "cultnet-ts", "tsconfig.json"), "--pretty", "false"]);
await run(process.execPath, [tsc, "-p", join(repoRoot, "packages", "cultmesh-browser", "tsconfig.json"), "--pretty", "false"]);
// esbuild strips types, so a package signature change would otherwise surface as a browser error. The page is
// type-checked against the same module map the bundle uses.
const moduleMap = {
"@gamecult/eve-contracts": join(eveRoot, "packages", "eve-contracts", "src", "index.ts"),
"@gamecult/eve-browser-lowering": join(eveRoot, "packages", "eve-browser-lowering", "src", "index.ts"),
"cultmesh-browser": join(repoRoot, "packages", "cultmesh-browser", "src", "index.ts"),
"cultnet-ts/contracts": join(repoRoot, "packages", "cultnet-ts", "src", "contracts.ts"),
};
const sampleTsconfig = join(workRoot, "tsconfig.json");
await writeFile(sampleTsconfig, JSON.stringify({
compilerOptions: {
target: "ES2022",
module: "ES2022",
moduleResolution: "Bundler",
lib: ["ES2022", "ESNext.Disposable", "DOM", "DOM.Iterable"],
strict: true,
noEmit: true,
skipLibCheck: true,
types: [],
paths: Object.fromEntries(Object.entries(moduleMap).map(([name, target]) => [name, [target]])),
},
files: [join(sampleRoot, "browser", "main.ts")],
}));
await run(process.execPath, [tsc, "-p", sampleTsconfig, "--pretty", "false"]);
await build({
entryPoints: [join(sampleRoot, "browser", "main.ts")],
outfile: bundlePath,
@ -48,12 +73,7 @@ try {
platform: "browser",
target: "es2022",
sourcemap: true,
alias: {
"@gamecult/eve-contracts": join(eveRoot, "packages", "eve-contracts", "src", "index.ts"),
"@gamecult/eve-browser-lowering": join(eveRoot, "packages", "eve-browser-lowering", "src", "index.ts"),
"cultmesh-browser": join(repoRoot, "packages", "cultmesh-browser", "src", "index.ts"),
"cultnet-ts/contracts": join(repoRoot, "packages", "cultnet-ts", "src", "contracts.ts"),
},
alias: moduleMap,
logLevel: "warning",
});
const bundle = await readFile(bundlePath, "utf8");