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:
parent
13b0b02d82
commit
195c37b867
2 changed files with 56 additions and 15 deletions
|
|
@ -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;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
Loading…
Reference in a new issue