CultLib/packages/cultmesh-browser/test/cultmesh-browser.test.ts

929 lines
32 KiB
TypeScript

import assert from "node:assert/strict";
import test from "node:test";
import { decode, encode } from "@msgpack/msgpack";
import {
encodeCultNetMessageForWire,
parseCultNetMessage,
type CultNetDatabaseSubscribeMessage,
type CultNetMessage,
type CultNetOperationRequestMessage,
type CultNetRawDocumentRecord,
type CultMeshVerseCatalogResponseMessage,
type CultMeshSessionOpenMessage,
} from "cultnet-ts/contracts";
import {
CultMeshBrowserClient,
CultMeshBrowserOperationError,
CultMeshBrowserOdinRendezvous,
decodeCultNetOperationPayload,
decodeCultNetPayload,
type CultMeshBrowserRoute,
type CultMeshBrowserP256PublicKey,
type CultMeshBrowserSocket,
} from "../src/index.js";
const localTrust = { mode: "local-development" as const };
test("Odin rendezvous resolves stable authority-runtime identity and observes route rotation", async () => {
const odin = new FakeOdin("ws://127.0.0.1:4050/mesh");
let id = 0;
const rendezvous = new CultMeshBrowserOdinRendezvous({
endpoints: ["ws://127.0.0.1:4040/odin"],
runtimeId: "browser-odin-test",
createId: () => `odin-${++id}`,
socketFactory: () => odin.open(),
});
assert.equal((await rendezvous.resolve({
verseId: "sample.counter",
authorityRuntimeId: "sample.counter-daemon",
}, localTrust)).endpoint, "ws://127.0.0.1:4050/mesh");
odin.providerEndpoint = "ws://127.0.0.1:4060/mesh";
assert.equal((await rendezvous.resolve({
verseId: "sample.counter",
authorityRuntimeId: "sample.counter-daemon",
}, localTrust)).endpoint, "ws://127.0.0.1:4060/mesh");
await assert.rejects(
rendezvous.resolve({ verseId: "sample.counter", authorityRuntimeId: "other-runtime" }, localTrust),
/could not resolve/,
);
});
test("Odin rendezvous never selects another authority's faster endpoint", async () => {
const odin = new FakeOdin("ws://127.0.0.1:4050/mesh");
odin.decoyEndpoint = "ws://127.0.0.1:4049/decoy";
const rendezvous = new CultMeshBrowserOdinRendezvous({
endpoints: ["ws://127.0.0.1:4040/odin"],
runtimeId: "browser-route-binding-test",
socketFactory: () => odin.open(),
});
const route = await rendezvous.resolve({
verseId: "sample.counter",
authorityRuntimeId: "sample.counter-daemon",
}, localTrust);
assert.equal(route.endpoint, "ws://127.0.0.1:4050/mesh");
});
test("Odin rendezvous accepts one-authority legacy routes and rejects ambiguous legacy catalogs", async () => {
const odin = new FakeOdin("ws://127.0.0.1:4070/mesh");
odin.omitAuthorityRoutes = true;
const rendezvous = new CultMeshBrowserOdinRendezvous({
endpoints: ["ws://127.0.0.1:4040/odin"],
runtimeId: "browser-legacy-route-test",
socketFactory: () => odin.open(),
});
const route = await rendezvous.resolve({
verseId: "sample.counter",
authorityRuntimeId: "sample.counter-daemon",
}, localTrust);
assert.equal(route.endpoint, "ws://127.0.0.1:4070/mesh");
odin.decoyEndpoint = "ws://127.0.0.1:4069/decoy";
await assert.rejects(
rendezvous.resolve({ verseId: "sample.counter", authorityRuntimeId: "sample.counter-daemon" }, localTrust),
(error: unknown) =>
error instanceof AggregateError &&
error.errors.some(inner => inner instanceof Error && /ambiguous legacy routes/.test(inner.message)),
);
});
test("Odin rendezvous rejects a poisoned route without vetoing a certified fallback", async () => {
const signed = await createSignedRoute("wss://provider.example/mesh");
const odin = new FakeOdin(signed.route.endpoint);
odin.authorityRoutesOverride = [
{
authorityRuntimeId: signed.route.authorityRuntimeId,
endpoint: "wss://poison.example/mesh",
protocolIds: ["cultmesh.documents.v1"],
priority: 0,
generation: "poisoned",
},
routeForCatalog(signed.route),
];
const rendezvous = new CultMeshBrowserOdinRendezvous({
endpoints: ["ws://127.0.0.1:4040/odin"],
runtimeId: "browser-certified-fallback-test",
socketFactory: () => odin.open(),
});
const selected = await rendezvous.resolve(
{ verseId: signed.route.verseId, authorityRuntimeId: signed.route.authorityRuntimeId },
{ mode: "authenticated-remote", odinRoots: [signed.odinPublic] },
);
assert.equal(selected.endpoint, signed.route.endpoint);
});
test("browser client leases provider state, invokes an operation, and follows route replacement", async () => {
const first = new FakeProvider("ws://127.0.0.1:4101/mesh", 4);
const second = new FakeProvider("ws://127.0.0.1:4102/mesh", 9);
let route = first.route;
let id = 0;
const client = await CultMeshBrowserClient.connect({
verseId: route.verseId,
authorityRuntimeId: route.authorityRuntimeId,
runtimeId: "browser-test",
trust: localTrust,
rendezvous: { resolve: async () => route },
createId: () => `id-${++id}`,
socketFactory: endpoint => endpoint === first.route.endpoint ? first.open() : second.open(),
});
const lease = await client.leaseRawDocument({
schemaId: "sample.counter_state.v1",
recordKey: "counter:main",
});
assert.deepEqual(decodeCultNetPayload<{ count: number }>(lease.current!).count, 4);
const observed: number[] = [];
const unwatch = lease.watch(record => {
if (record) observed.push(decodeCultNetPayload<{ count: number }>(record).count);
});
const response = await client.invoke({
serviceId: "sample.counter",
operation: "increment",
payloadSchema: "sample.increment.v1",
payload: { amount: 2 },
});
assert.equal(response.status, "accepted");
assert.deepEqual(decodeCultNetOperationPayload<{ count: number }>(response), { count: 6 });
assert.deepEqual(observed, [4, 6]);
route = second.route;
await client.refreshRoute();
assert.equal(client.state, "connected");
assert.deepEqual(decodeCultNetPayload<{ count: number }>(lease.current!).count, 9);
assert.deepEqual(observed, [4, 6, 9]);
assert.equal(first.activeSubscriptionCount, 0);
assert.equal(second.activeSubscriptionCount, 1);
unwatch();
lease.dispose();
assert.equal(second.activeSubscriptionCount, 0);
await client.dispose();
});
test("browser client rejects wrong rendezvous identity and has no document write API", async () => {
const provider = new FakeProvider("ws://127.0.0.1:4201/mesh", 1);
await assert.rejects(
CultMeshBrowserClient.connect({
verseId: "sample.counter",
authorityRuntimeId: "sample.counter-daemon",
runtimeId: "browser-test",
trust: localTrust,
rendezvous: {
resolve: async () => ({
...provider.route,
authorityRuntimeId: "intruder-runtime",
}),
},
socketFactory: () => provider.open(),
}),
/wrong stable identity/,
);
assert.equal("putDocument" in CultMeshBrowserClient.prototype, false);
assert.equal("setDocument" in CultMeshBrowserClient.prototype, false);
});
test("browser client rejects a route whose peer proves the wrong authority", async () => {
const provider = new FakeProvider("ws://127.0.0.1:4251/mesh", 1, false, "intruder-runtime");
await assert.rejects(
CultMeshBrowserClient.connect({
verseId: "sample.counter",
authorityRuntimeId: "sample.counter-daemon",
runtimeId: "browser-peer-proof-test",
trust: localTrust,
rendezvous: { resolve: async () => ({ ...provider.route, authorityRuntimeId: "sample.counter-daemon" }) },
socketFactory: () => provider.open(),
}),
/proved .*intruder-runtime.*expected .*sample.counter-daemon/,
);
});
test("browser client accepts an Odin-certified route only after provider nonce proof", async () => {
const fixture = await createSignedRoute("wss://provider.example/mesh");
const client = await CultMeshBrowserClient.connect({
...fixture.route,
runtimeId: "browser-authenticated-test",
rendezvous: { resolve: async () => fixture.route },
trust: { mode: "authenticated-remote", odinRoots: [fixture.odinPublic] },
socketFactory: () => signedHandshakeSocket(fixture.route, fixture.providerPrivate),
});
assert.equal(client.state, "connected");
await client.dispose();
});
test("browser client rejects a credential-free authority impersonator", async () => {
const fixture = await createSignedRoute("wss://provider.example/mesh");
const impersonator = new FakeProvider("wss://provider.example/mesh", 1);
await assert.rejects(
CultMeshBrowserClient.connect({
...fixture.route,
runtimeId: "browser-impersonation-test",
rendezvous: { resolve: async () => fixture.route },
trust: { mode: "authenticated-remote", odinRoots: [fixture.odinPublic] },
socketFactory: () => impersonator.open(),
}),
/did not prove possession/,
);
});
test("browser client rejects a mutated or expired signed route before opening a provider socket", async () => {
const fixture = await createSignedRoute("wss://provider.example/mesh");
let opened = false;
const connect = (route: CultMeshBrowserRoute, now?: () => number) => CultMeshBrowserClient.connect({
...route,
runtimeId: "browser-route-certificate-test",
rendezvous: { resolve: async () => route },
trust: { mode: "authenticated-remote", odinRoots: [fixture.odinPublic], now },
socketFactory: () => {
opened = true;
return signedHandshakeSocket(route, fixture.providerPrivate);
},
});
await assert.rejects(connect({ ...fixture.route, endpoint: "wss://evil.example/mesh" }), /signature is invalid/);
assert.equal(opened, false);
await assert.rejects(connect(fixture.route, () => fixture.expiresAt), /not currently valid/);
assert.equal(opened, false);
});
test("browser client bounds unanswered provider operations", async () => {
const route = {
verseId: "sample.counter",
authorityRuntimeId: "sample.counter-daemon",
endpoint: "ws://127.0.0.1:4301/mesh",
generation: "route-4301",
};
const client = await CultMeshBrowserClient.connect({
...route,
runtimeId: "browser-timeout-test",
trust: localTrust,
rendezvous: { resolve: async () => route },
requestTimeoutMs: 5,
socketFactory: () => handshakeOnlySocket(route),
});
await assert.rejects(
client.invoke({
serviceId: "sample.counter",
operation: "increment",
payloadSchema: "sample.increment.v1",
payload: { amount: 1 },
idempotencyKey: "timeout-1",
}),
/timed out/,
);
await client.dispose();
});
test("browser client rejects a correlated framework failure without waiting for timeout", async () => {
const provider = new FakeProvider("ws://127.0.0.1:4351/mesh", 1, true);
const client = await CultMeshBrowserClient.connect({
...provider.route,
runtimeId: "browser-failure-test",
trust: localTrust,
rendezvous: { resolve: async () => provider.route },
requestTimeoutMs: 60_000,
socketFactory: () => provider.open(),
});
await assert.rejects(
client.invoke({
serviceId: "sample.counter",
operation: "increment",
payloadSchema: "wrong.request.v1",
payload: { amount: 1 },
idempotencyKey: "failure-1",
}),
(error: unknown) => {
assert.ok(error instanceof CultMeshBrowserOperationError);
assert.equal(error.status, "invalid");
assert.equal(error.code, "request-schema-mismatch");
assert.match(error.message, /expected payload schema/i);
return true;
},
);
await client.dispose();
});
test("browser client replays one idempotent operation after route loss without duplicating its effect", async () => {
const firstRoute = localRoute("ws://127.0.0.1:4311/mesh", "generation-1");
const secondRoute = localRoute("ws://127.0.0.1:4312/mesh", "generation-2");
let route = firstRoute;
let effectCount = 0;
let dropFirstResponse = true;
const results = new Map<string, CultNetMessage>();
const open = () => {
let socket: FakeSocket;
socket = new FakeSocket(wire => {
const message = parseCultNetMessage(decode(wire));
if (message.schemaVersion === "cultmesh.session_open.v2") {
socket.deliver({
schemaVersion: "cultmesh.session_accepted.v2",
messageId: message.messageId,
accepted: true,
verseId: message.verseId,
authorityRuntimeId: message.authorityRuntimeId,
protocolId: message.protocolId,
routeGeneration: message.routeGeneration,
clientNonce: message.clientNonce,
providerKeyId: "",
providerSignature: "",
});
return;
}
if (message.schemaVersion !== "cultnet.operation_request.v0") return;
let response = results.get(message.messageId);
if (!response) {
effectCount++;
response = {
schemaVersion: "cultnet.operation_response.v0",
messageId: message.messageId,
serviceId: message.serviceId,
operation: message.operation,
status: "accepted",
payloadSchema: "sample.increment_receipt.v1",
payloadEncoding: "messagepack-base64",
payload: Buffer.from(encode({ count: effectCount })).toString("base64"),
sourceRuntimeId: "sample.counter-daemon",
};
results.set(message.messageId, response!);
}
if (dropFirstResponse) {
dropFirstResponse = false;
route = secondRoute;
socket.close(1012, "route lost after commit");
return;
}
socket.deliver(response!);
}, () => undefined);
return socket;
};
const client = await CultMeshBrowserClient.connect({
...firstRoute,
runtimeId: "browser-operation-replay-test",
rendezvous: { resolve: async () => route },
trust: localTrust,
reconnectDelayMs: 1,
requestTimeoutMs: 2_000,
socketFactory: () => open(),
});
const response = await client.invoke({
serviceId: "sample.counter",
operation: "increment",
payloadSchema: "sample.increment.v1",
payload: { amount: 1 },
idempotencyKey: "replay-once",
});
assert.deepEqual(decodeCultNetOperationPayload<{ count: number }>(response), { count: 1 });
assert.equal(effectCount, 1);
await client.dispose();
});
test("an early caller reconnect cancels the scheduled reconnect and keeps one live socket", async () => {
const provider = new FakeProvider("ws://127.0.0.1:4361/mesh", 2);
const client = await CultMeshBrowserClient.connect({
...provider.route,
runtimeId: "browser-single-reconnect-test",
rendezvous: { resolve: async () => provider.route },
trust: localTrust,
reconnectDelayMs: 40,
socketFactory: () => provider.open(),
});
const lease = await client.leaseRawDocument({
schemaId: "sample.counter_state.v1",
recordKey: "counter:main",
});
provider.dropConnections();
await client.invoke({
serviceId: "sample.counter",
operation: "increment",
payloadSchema: "sample.increment.v1",
payload: { amount: 1 },
});
await new Promise(resolve => setTimeout(resolve, 80));
assert.equal(provider.openCount, 2);
assert.equal(provider.liveSocketCount, 1);
assert.equal(provider.activeSubscriptionCount, 1);
lease.dispose();
await client.dispose();
});
test("browser client rejects an operation response from another runtime", async () => {
const provider = new FakeProvider(
"ws://127.0.0.1:4371/mesh",
1,
false,
"sample.counter-daemon",
"intruder-runtime",
);
const client = await CultMeshBrowserClient.connect({
...provider.route,
runtimeId: "browser-response-authority-test",
trust: localTrust,
rendezvous: { resolve: async () => provider.route },
socketFactory: () => provider.open(),
});
await assert.rejects(
client.invoke({
serviceId: "sample.counter",
operation: "increment",
payloadSchema: "sample.increment.v1",
payload: { amount: 1 },
}),
/response came from 'intruder-runtime'/,
);
await client.dispose();
});
test("browser client rejects oversized outbound schema messages", async () => {
const route = {
verseId: "sample.counter",
authorityRuntimeId: "sample.counter-daemon",
endpoint: "ws://127.0.0.1:4401/mesh",
generation: "route-4401",
};
await assert.rejects(
CultMeshBrowserClient.connect({
...route,
runtimeId: "browser-size-test",
trust: localTrust,
rendezvous: { resolve: async () => route },
maxFrameBytes: 8,
socketFactory: () => new FakeSocket(() => undefined, () => undefined),
}),
/exceeds the 8-byte limit/,
);
});
test("browser client reserves subscription identity while the initial lease opens", async () => {
const provider = new FakeProvider("ws://127.0.0.1:4501/mesh", 3);
const client = await CultMeshBrowserClient.connect({
...provider.route,
runtimeId: "browser-lease-race-test",
trust: localTrust,
rendezvous: { resolve: async () => provider.route },
socketFactory: () => provider.open(),
});
const first = client.leaseRawDocument({
schemaId: "sample.counter_state.v1",
recordKey: "counter:main",
subscriptionId: "one-owner",
});
await assert.rejects(
client.leaseRawDocument({
schemaId: "sample.counter_state.v1",
recordKey: "counter:main",
subscriptionId: "one-owner",
}),
/already leased/,
);
(await first).dispose();
await client.dispose();
});
class FakeProvider {
readonly route: CultMeshBrowserRoute;
#count: number;
#subscriptions = new Map<FakeSocket, Map<string, CultNetDatabaseSubscribeMessage>>();
#rejectOperations: boolean;
#runtimeId: string;
#responseRuntimeId: string;
#openCount = 0;
constructor(
endpoint: string,
count: number,
rejectOperations = false,
runtimeId = "sample.counter-daemon",
responseRuntimeId = runtimeId,
) {
this.route = {
verseId: "sample.counter",
authorityRuntimeId: "sample.counter-daemon",
endpoint,
generation: endpoint,
};
this.#count = count;
this.#rejectOperations = rejectOperations;
this.#runtimeId = runtimeId;
this.#responseRuntimeId = responseRuntimeId;
}
get activeSubscriptionCount(): number {
let count = 0;
for (const subscriptions of this.#subscriptions.values()) count += subscriptions.size;
return count;
}
get openCount(): number {
return this.#openCount;
}
get liveSocketCount(): number {
return this.#subscriptions.size;
}
dropConnections(): void {
for (const socket of [...this.#subscriptions.keys()]) socket.close(1012, "route dropped");
}
open(): FakeSocket {
this.#openCount++;
const socket = new FakeSocket(
wire => this.receive(socket, wire),
() => this.#subscriptions.delete(socket),
);
this.#subscriptions.set(socket, new Map());
return socket;
}
private receive(socket: FakeSocket, wire: Uint8Array): void {
const message = parseCultNetMessage(decode(wire));
switch (message.schemaVersion) {
case "cultmesh.session_open.v2":
this.acceptSession(socket, message);
return;
case "cultnet.database_subscribe.v0":
this.subscribe(socket, message);
return;
case "cultnet.database_unsubscribe.v0":
this.#subscriptions.get(socket)?.delete(message.subscriptionId);
return;
case "cultnet.operation_request.v0":
this.invoke(socket, message);
return;
default:
return;
}
}
private acceptSession(socket: FakeSocket, message: CultMeshSessionOpenMessage): void {
socket.deliver({
schemaVersion: "cultmesh.session_accepted.v2",
messageId: message.messageId,
accepted: true,
verseId: message.verseId,
authorityRuntimeId: this.#runtimeId,
protocolId: message.protocolId,
routeGeneration: message.routeGeneration,
clientNonce: message.clientNonce,
providerKeyId: "",
providerSignature: "",
});
}
private subscribe(socket: FakeSocket, message: CultNetDatabaseSubscribeMessage): void {
this.#subscriptions.get(socket)!.set(message.subscriptionId, message);
socket.deliver({
schemaVersion: "cultnet.snapshot_response_raw.v0",
messageId: message.messageId,
documents: [this.record()],
});
}
private invoke(socket: FakeSocket, message: CultNetOperationRequestMessage): void {
assert.equal(message.targetRuntimeId, this.#runtimeId);
if (this.#rejectOperations) {
socket.deliver({
schemaVersion: "cultnet.operation_response.v0",
messageId: message.messageId,
serviceId: message.serviceId,
operation: message.operation,
status: "invalid",
payloadSchema: "gamecult.cultnet.operation_failure.v1",
payloadEncoding: "messagepack-base64",
payload: bytesToBase64(encode({
code: "request-schema-mismatch",
message: "Expected payload schema 'sample.increment.v1'.",
})),
diagnostics: ["Request schema did not match."],
sourceRuntimeId: this.#responseRuntimeId,
});
return;
}
const payload = decode(base64ToBytes(message.payload)) as { amount: number };
this.#count += payload.amount;
for (const [peer, subscriptions] of this.#subscriptions) {
for (const subscriptionId of subscriptions.keys()) {
peer.deliver({
schemaVersion: "cultnet.database_change_raw.v0",
messageId: `change-${message.messageId}`,
subscriptionId,
changeKind: "updated",
document: this.record(),
});
}
}
socket.deliver({
schemaVersion: "cultnet.operation_response.v0",
messageId: message.messageId,
serviceId: message.serviceId,
operation: message.operation,
status: "accepted",
payloadSchema: "sample.increment_receipt.v1",
payloadEncoding: "messagepack-base64",
payload: bytesToBase64(encode({ count: this.#count })),
sourceRuntimeId: this.#responseRuntimeId,
});
}
private record(): CultNetRawDocumentRecord {
return {
schemaId: "sha256:sample-counter-state",
schemaName: "sample.counter_state",
schemaVersion: "sample.counter_state.v1",
schemaContentHash: "sha256:sample-counter-state-content",
recordKey: "counter:main",
storedAt: "2026-08-17T00:00:00Z",
payloadEncoding: "messagepack",
payload: encode({ count: this.#count }),
sourceRuntimeId: this.#runtimeId,
};
}
}
class FakeOdin {
providerEndpoint: string;
decoyEndpoint: string | undefined;
omitAuthorityRoutes = false;
authorityRoutesOverride: NonNullable<CultMeshVerseCatalogResponseMessage["verses"][number]["authorityRoutes"]> | undefined;
constructor(providerEndpoint: string) {
this.providerEndpoint = providerEndpoint;
}
open(): FakeSocket {
const socket = new FakeSocket(wire => {
const message = parseCultNetMessage(decode(wire));
if (message.schemaVersion !== "cultmesh.verse_catalog_request.v0") return;
socket.deliver({
schemaVersion: "cultmesh.verse_catalog_response.v0",
messageId: message.messageId,
verses: [{
verseId: "sample.counter",
displayName: "Counter",
authorityModel: "ServerAuthoritative",
compatibility: {
transportVersion: "cultmesh.v1",
rulesHash: "counter-v1",
compatibleVerseIds: [],
requiredPluginIds: [],
optionalPluginIds: [],
},
discoveryEndpoints: [this.providerEndpoint],
authorityRuntimeIds: this.decoyEndpoint
? ["sample.counter-daemon", "sample.decoy-daemon"]
: ["sample.counter-daemon"],
...(!this.omitAuthorityRoutes ? { authorityRoutes: this.authorityRoutesOverride ?? [
...(this.decoyEndpoint ? [{
authorityRuntimeId: "sample.decoy-daemon",
endpoint: this.decoyEndpoint,
protocolIds: ["cultmesh.documents.v1"],
priority: 0,
generation: "decoy-generation",
}] : []),
{
authorityRuntimeId: "sample.counter-daemon",
endpoint: this.providerEndpoint,
protocolIds: ["cultmesh.documents.v1"],
priority: 10,
generation: this.providerEndpoint,
},
] } : {}),
}],
});
}, () => undefined);
return socket;
}
}
function routeForCatalog(route: CultMeshBrowserRoute): NonNullable<
CultMeshVerseCatalogResponseMessage["verses"][number]["authorityRoutes"]
>[number] {
return {
authorityRuntimeId: route.authorityRuntimeId,
endpoint: route.endpoint,
protocolIds: [...(route.protocolIds ?? [route.protocolId ?? "cultmesh.documents.v1"])],
priority: route.priority ?? 0,
generation: route.generation,
...(route.certificate ? {
certificate: {
providerKeyId: route.certificate.providerKey.keyId,
providerPublicKeyX: route.certificate.providerKey.x,
providerPublicKeyY: route.certificate.providerKey.y,
odinKeyId: route.certificate.odinKeyId,
issuedAtUnixMilliseconds: route.certificate.issuedAtUnixMilliseconds,
expiresAtUnixMilliseconds: route.certificate.expiresAtUnixMilliseconds,
signature: route.certificate.signature,
},
} : {}),
};
}
function handshakeOnlySocket(route: CultMeshBrowserRoute): FakeSocket {
let socket: FakeSocket;
socket = new FakeSocket(wire => {
const message = parseCultNetMessage(decode(wire));
if (message.schemaVersion !== "cultmesh.session_open.v2") return;
socket.deliver({
schemaVersion: "cultmesh.session_accepted.v2",
messageId: message.messageId,
accepted: true,
verseId: route.verseId,
authorityRuntimeId: route.authorityRuntimeId,
protocolId: message.protocolId,
routeGeneration: message.routeGeneration,
clientNonce: message.clientNonce,
providerKeyId: "",
providerSignature: "",
});
}, () => undefined);
return socket;
}
function localRoute(endpoint: string, generation: string): CultMeshBrowserRoute {
return {
verseId: "sample.counter",
authorityRuntimeId: "sample.counter-daemon",
endpoint,
protocolId: "cultmesh.documents.v1",
protocolIds: ["cultmesh.documents.v1"],
priority: 0,
generation,
};
}
class FakeSocket implements CultMeshBrowserSocket {
binaryType: BinaryType = "arraybuffer";
readyState = 0;
onopen: ((event: Event) => void) | null = null;
onmessage: ((event: MessageEvent) => void) | null = null;
onerror: ((event: Event) => void) | null = null;
onclose: ((event: CloseEvent) => void) | null = null;
#receive: (wire: Uint8Array) => void;
#closed: () => void;
constructor(receive: (wire: Uint8Array) => void, closed: () => void) {
this.#receive = receive;
this.#closed = closed;
queueMicrotask(() => {
this.readyState = 1;
this.onopen?.({} as Event);
});
}
send(data: ArrayBuffer | ArrayBufferView): void {
if (this.readyState !== 1) throw new Error("Fake socket is not open.");
const wire = data instanceof ArrayBuffer
? new Uint8Array(data)
: new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
this.#receive(wire);
}
close(code = 1000, reason = ""): void {
if (this.readyState === 3) return;
this.readyState = 3;
this.#closed();
this.onclose?.({ code, reason } as CloseEvent);
}
deliver(message: CultNetMessage): void {
const wire = encode(encodeCultNetMessageForWire(message));
queueMicrotask(() => this.onmessage?.({ data: wire.slice().buffer } as MessageEvent));
}
}
async function createSignedRoute(endpoint: string): Promise<{
route: CultMeshBrowserRoute;
odinPublic: CultMeshBrowserP256PublicKey;
providerPrivate: CryptoKey;
expiresAt: number;
}> {
const odin = await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, true, ["sign", "verify"]);
const provider = await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, true, ["sign", "verify"]);
const odinPublic = await exportPublic("odin-root-1", odin.publicKey);
const providerPublic = await exportPublic("provider-1", provider.publicKey);
const issuedAt = Date.now() - 1_000;
const expiresAt = Date.now() + 60_000;
const unsigned: CultMeshBrowserRoute = {
verseId: "sample.counter",
authorityRuntimeId: "sample.counter-daemon",
endpoint,
protocolId: "cultmesh.documents.v1",
protocolIds: ["cultmesh.documents.v1"],
priority: 0,
generation: "signed-generation-1",
certificate: {
providerKey: providerPublic,
odinKeyId: odinPublic.keyId,
issuedAtUnixMilliseconds: issuedAt,
expiresAtUnixMilliseconds: expiresAt,
signature: "",
},
};
const signature = await crypto.subtle.sign(
{ name: "ECDSA", hash: "SHA-256" },
odin.privateKey,
testCanonicalRoute(unsigned).slice().buffer as ArrayBuffer,
);
return {
route: { ...unsigned, certificate: { ...unsigned.certificate!, signature: toBase64(new Uint8Array(signature)) } },
odinPublic,
providerPrivate: provider.privateKey,
expiresAt,
};
}
function signedHandshakeSocket(route: CultMeshBrowserRoute, providerPrivate: CryptoKey): FakeSocket {
let socket: FakeSocket;
socket = new FakeSocket(wire => {
const message = parseCultNetMessage(decode(wire));
if (message.schemaVersion !== "cultmesh.session_open.v2") return;
void crypto.subtle.sign(
{ name: "ECDSA", hash: "SHA-256" },
providerPrivate,
testCanonicalSession(message, route.endpoint).slice().buffer as ArrayBuffer,
).then(signature => socket.deliver({
schemaVersion: "cultmesh.session_accepted.v2",
messageId: message.messageId,
accepted: true,
verseId: message.verseId,
authorityRuntimeId: message.authorityRuntimeId,
protocolId: message.protocolId,
routeGeneration: message.routeGeneration,
clientNonce: message.clientNonce,
providerKeyId: route.certificate!.providerKey.keyId,
providerSignature: toBase64(new Uint8Array(signature)),
}));
}, () => undefined);
return socket;
}
async function exportPublic(keyId: string, key: CryptoKey): Promise<CultMeshBrowserP256PublicKey> {
const jwk = await crypto.subtle.exportKey("jwk", key);
return { keyId, x: urlBase64ToBase64(jwk.x!), y: urlBase64ToBase64(jwk.y!) };
}
function testCanonicalRoute(route: CultMeshBrowserRoute): Uint8Array {
const certificate = route.certificate!;
return testCanonical(
"gamecult.cultmesh.route-certificate.v1", route.verseId, route.authorityRuntimeId, route.endpoint,
[...route.protocolIds!].sort().join("\u001f"), String(route.priority), route.generation,
certificate.providerKey.keyId, certificate.providerKey.x, certificate.providerKey.y,
certificate.odinKeyId, String(certificate.issuedAtUnixMilliseconds), String(certificate.expiresAtUnixMilliseconds),
);
}
function testCanonicalSession(message: CultMeshSessionOpenMessage, endpoint: string): Uint8Array {
return testCanonical(
"gamecult.cultmesh.session-proof.v1", message.clientNonce, message.messageId, message.sourceRuntimeId,
message.verseId, message.authorityRuntimeId, message.protocolId, endpoint, message.routeGeneration,
);
}
function testCanonical(...values: string[]): Uint8Array {
const chunks = values.map(value => new TextEncoder().encode(value));
const bytes = new Uint8Array(chunks.reduce((sum, chunk) => sum + 4 + chunk.length, 0));
const view = new DataView(bytes.buffer);
let offset = 0;
for (const chunk of chunks) {
view.setUint32(offset, chunk.length, false);
offset += 4;
bytes.set(chunk, offset);
offset += chunk.length;
}
return bytes;
}
function toBase64(bytes: Uint8Array): string {
return Buffer.from(bytes).toString("base64");
}
function urlBase64ToBase64(value: string): string {
return Buffer.from(value, "base64url").toString("base64");
}
function bytesToBase64(bytes: Uint8Array): string {
let binary = "";
for (const byte of bytes) binary += String.fromCharCode(byte);
return btoa(binary);
}
function base64ToBytes(value: string): Uint8Array {
const binary = atob(value);
return Uint8Array.from(binary, character => character.charCodeAt(0));
}