Add provider session wire transport

This commit is contained in:
Meta Vens 2026-07-14 23:47:30 +01:00
commit 262b5cc53d
10 changed files with 2092 additions and 5 deletions

View file

@ -0,0 +1,61 @@
# CultMesh Provider Session
## Authority map
- Owner: the provider-session broker owns registration, active lease fencing,
accepted publication membership, command retention, receipt acceptance, and
withdrawal.
- Inputs: a physical CultNet session, an explicit registration-authorization
decision, correlated typed operations, the broker clock, provider commands,
and persistence callbacks. The current RUDP adapter does not itself provide
an authenticated claim.
- Outputs: leases, application acceptances, accepted raw documents, pushed
typed commands, persisted receipts, and deterministic removals.
- Derived state: socket sessions, retry schedules, connection health, and
provider projections are observations. They do not authorize publications.
- Forbidden writers: RUDP packet acknowledgements, source addresses, direct raw
document callbacks, renderer-owned catalogs, and arrival-time freshness maps
cannot create accepted provider truth.
- Shared paths: initial connect and reconnect use `provider.register`; initial,
replayed, and live documents use `provider.publication.put`; explicit
withdrawal and lease expiry remove the broker-owned publication set; command
retry ends only when `provider.receipt.put` is accepted.
- Cut line: a receiver must stop accepting unleased provider raw puts before it
claims this protocol as its ingress authority.
## Wire contract
Provider lifecycle operations use the reliable ordered CultNet schema channel
and the existing `cultnet.operation_request.v0` / `cultnet.operation_response.v0`
envelopes with service id `gamecult.mesh.provider_session`. Inner payloads are
camel-case MessagePack maps encoded as `messagepack-base64`.
The v1 operations are:
| Operation | Request payload | Successful payload |
| --- | --- | --- |
| `provider.register` | `gamecult.mesh.provider_registration.v1` | `gamecult.mesh.provider_lease.v1` |
| `provider.renew` | `gamecult.mesh.provider_lease_renewal.v1` | `gamecult.mesh.provider_lease.v1` |
| `provider.publication.put` | `gamecult.mesh.provider_publication_put.v1` | `gamecult.mesh.provider_mutation_acceptance.v1` |
| `provider.publication.delete` | `gamecult.mesh.provider_publication_delete.v1` | `gamecult.mesh.provider_mutation_acceptance.v1` |
| `provider.receipt.put` | `gamecult.mesh.provider_receipt_put.v1` | `gamecult.mesh.provider_mutation_acceptance.v1` |
| `provider.withdraw` | `gamecult.mesh.provider_withdrawal.v1` | `gamecult.mesh.provider_mutation_acceptance.v1` |
Broker-to-provider commands are raw typed documents using
`gamecult.mesh.provider_command.v1`. Asset bodies do not travel inside these
operation payloads.
Responses use `ok`, `conflict`, `expired`, `denied`, or `invalid`. A transport
ACK is never an application acceptance. Renewal issues a new lease id and
immediately fences the previous id. The broker derives the set removed by full
withdrawal; a provider cannot use a supplied list to hide owned publications.
Public deployment additionally requires a CultNet authentication claim that
authorizes the provider identity. Until that claim is wired and checked, the
TypeScript RUDP adapter is a private-development transport.
`CultMeshProviderSessionBroker` is an in-memory reference broker and socket
proof. Its command and receipt retention does not survive process restart.
Production receivers must persist that control-plane state atomically with
accepted publication ownership; Odin must not substitute these volatile maps
for its durable broker authority.

View file

@ -271,7 +271,16 @@ publication, and withdrawal. Provider code owns domain state and idempotent
command transactions; the injected transport owns CultNet wire details.
```ts
import { CultMeshProviderSession } from "cultmesh-ts";
import {
CultMeshProviderRudpTransport,
CultMeshProviderSession,
} from "cultmesh-ts";
const odinTransport = new CultMeshProviderRudpTransport({
endpoint: "rudp://127.0.0.1:17871",
runtimeId: "voidbot-worker-7",
connectionId: 0x43554c54,
});
const session = new CultMeshProviderSession({
identity: {
@ -300,6 +309,19 @@ is persisted before transport publication, remains pending across reconnects,
and is marked published only after the current connection accepts it. Receipt
store failure degrades command intake without inventing a network outage.
The RUDP provider transport carries lifecycle payloads inside the existing
`cultnet.operation_request.v0` and `cultnet.operation_response.v0` envelopes.
RUDP acknowledgements prove byte delivery only. Registration, renewal,
publication changes, receipts, and withdrawal complete only after the
provider-session broker returns a correlated application response. The broker
owns lease fencing and accepted publication membership; the provider owns its
desired documents and durable receipt outbox.
`CultMeshProviderRudpTransport` is currently a private-development transport.
Do not advertise it as a public provider boundary until the surrounding
CultNet session authenticates a claim authorizing the four-part provider
identity. Source address and successful contact are not authority.
## RUDP Helpers
Node runtimes can build the shared CultNet reliable-UDP transport from the

View file

@ -25,6 +25,8 @@ import {
type CultNetErrorMessage,
type CultNetDocumentBinding,
type CultNetMessage,
type CultNetOperationRequestMessage,
type CultNetOperationResponseMessage,
type CultNetRawDocumentRecord,
type CultNetReconnectPolicy,
type CultNetSchemaCatalogOptions,
@ -33,6 +35,9 @@ import {
} from "cultnet-ts";
export * from "./provider-session";
export * from "./provider-session-wire";
export * from "./provider-rudp-transport";
export * from "./provider-session-broker";
export interface CultMeshVec2 {
readonly x: number;
@ -4095,10 +4100,21 @@ export interface CultMeshRudpDocumentServerOptions extends CultMeshRudpSocketOpt
getCache?: () => Promise<CultCache> | CultCache;
onError?: (error: Error) => void;
onDocumentPutRaw?: (document: CultMeshRudpDocumentPut) => void | Promise<void>;
onOperationRequest?: (
request: CultNetOperationRequestMessage,
session: CultMeshRudpServerSession,
) => CultNetOperationResponseMessage | Promise<CultNetOperationResponseMessage>;
onSessionClosed?: (session: CultMeshRudpServerSession) => void | Promise<void>;
wireContract?: CultNetWireContract;
sessionTimeoutMs?: number;
}
export interface CultMeshRudpServerSession {
readonly sessionId: string;
readonly remote: { address: string; family: string; port: number };
send(message: CultNetMessage): void;
}
export interface CultMeshRudpDocumentServer {
readonly bind: { host: string; port: number };
start(): Promise<void>;
@ -5542,9 +5558,12 @@ export class CultMesh {
const bind = { host, port };
const socket = options.socket ?? createSocket(host.includes(":") ? "udp6" : "udp4");
const sessions = new Map<string, {
sessionId: string;
remote: { address: string; family: string; port: number };
session: CultNetRudpSession;
work: Promise<void>;
}>();
let sessionSequence = 0;
const resendPollMs = Math.max(10, options.resendPollMs ?? 25);
const sessionTimeoutMs = Math.max(1_000, options.sessionTimeoutMs ?? 30_000);
const wireContract = options.wireContract ?? "cultnet.schema.v0";
@ -5567,7 +5586,9 @@ export class CultMesh {
let record = sessions.get(key);
if (packet.packetType === "connect") {
if (record) notifySessionClosed(record);
record = {
sessionId: `${runtimeId}:${++sessionSequence}`,
remote: { address: remote.address, family: remote.family, port: remote.port },
session: new CultNetRudpSession({
connectionId,
@ -5575,6 +5596,7 @@ export class CultMesh {
resendDelayMs: options.resendDelayMs,
maxPendingReliablePackets: options.maxPendingReliablePackets,
}),
work: Promise.resolve(),
};
sessions.set(key, record);
socket.send(encodeRudpPacket(record.session.acceptConnect(packet, nowMs)), remote.port, remote.address);
@ -5593,12 +5615,13 @@ export class CultMesh {
if (frame.channelId !== "schema") {
continue;
}
handleDocumentServerFrame(record, frame.payload).catch((error) => {
reportError(error);
});
record.work = record.work
.then(() => handleDocumentServerFrame(record, frame.payload))
.catch(reportError);
}
if (result.disconnected) {
sessions.delete(key);
notifySessionClosed(record);
return;
}
if (packet.packetType === "accept" || result.delivered.length > 0) {
@ -5611,7 +5634,7 @@ export class CultMesh {
socket.on("error", reportError);
async function handleDocumentServerFrame(
record: { remote: { address: string; family: string; port: number }; session: CultNetRudpSession },
record: { sessionId: string; remote: { address: string; family: string; port: number }; session: CultNetRudpSession },
payload: Uint8Array,
): Promise<void> {
const message = parseCultNetMessage(decode(payload), wireContract);
@ -5643,6 +5666,16 @@ export class CultMesh {
await options.onDocumentPutRaw(normalizeRudpDocumentPut(message, record.remote));
}
return;
case "cultnet.operation_request.v0":
if (!options.onOperationRequest) {
sendSchemaMessage(record, {
schemaVersion: "cultnet.error.v0",
error: "CultMesh RUDP document server has no operation handler.",
});
return;
}
sendSchemaMessage(record, await options.onOperationRequest(message, publicSession(record)));
return;
default:
sendSchemaMessage(record, {
schemaVersion: "cultnet.error.v0",
@ -5651,6 +5684,27 @@ export class CultMesh {
}
}
function publicSession(record: {
sessionId: string;
remote: { address: string; family: string; port: number };
session: CultNetRudpSession;
}): CultMeshRudpServerSession {
return {
sessionId: record.sessionId,
remote: { ...record.remote },
send: message => sendSchemaMessage(record, message),
};
}
function notifySessionClosed(record: {
sessionId: string;
remote: { address: string; family: string; port: number };
session: CultNetRudpSession;
}): void {
if (!options.onSessionClosed) return;
void Promise.resolve(options.onSessionClosed(publicSession(record))).catch(reportError);
}
function sendSchemaMessage(
record: { remote: { address: string; port: number }; session: CultNetRudpSession },
message: CultNetMessage,
@ -5684,6 +5738,7 @@ export class CultMesh {
for (const [key, record] of sessions) {
if (record.session.checkTimeout(nowMs, sessionTimeoutMs)) {
sessions.delete(key);
notifySessionClosed(record);
continue;
}
for (const packet of record.session.dueResends(nowMs)) {
@ -5698,6 +5753,7 @@ export class CultMesh {
clearInterval(resendTimer);
resendTimer = undefined;
}
for (const record of sessions.values()) notifySessionClosed(record);
sessions.clear();
socket.close();
},

View file

@ -0,0 +1,390 @@
import { randomUUID } from "node:crypto";
import { createSocket, type Socket } from "node:dgram";
import { encode } from "@msgpack/msgpack";
import {
CultNetPeer,
CultNetRudpSocketTransportConnection,
type CultNetMessage,
type CultNetOperationRequestMessage,
type CultNetOperationResponseMessage,
type CultNetRawDocumentRecord,
} from "cultnet-ts";
import type {
CultMeshProviderCommand,
CultMeshProviderCommandListener,
CultMeshProviderCommandReceipt,
CultMeshProviderConnection,
CultMeshProviderIdentity,
CultMeshProviderLease,
CultMeshProviderPublication,
CultMeshProviderRegistration,
CultMeshProviderTransport,
CultMeshProviderUnsubscribe,
CultMeshProviderWithdrawal,
} from "./provider-session";
import {
CULTMESH_PROVIDER_SESSION_SERVICE_ID,
assertMutationAcceptance,
assertProviderLease,
cultMeshProviderSessionOperations,
cultMeshProviderSessionSchemas,
decodeProviderCommandDocument,
decodeProviderSessionPayload,
encodeProviderSessionPayload,
type CultMeshProviderCommandReceiptWire,
type CultMeshProviderLeaseWire,
type CultMeshProviderMutationAcceptanceWire,
} from "./provider-session-wire";
export interface CultMeshProviderRudpTransportOptions {
readonly endpoint: string;
readonly runtimeId: string;
readonly connectionId: number;
readonly bindHost?: string;
readonly connectTimeoutMs?: number;
readonly operationTimeoutMs?: number;
readonly maxFragmentBytes?: number;
readonly socketFactory?: () => Socket;
}
/**
* Private-development provider transport. Public deployment additionally needs
* an authenticated CultNet session whose claims authorize the provider identity.
*/
export class CultMeshProviderRudpTransport implements CultMeshProviderTransport {
readonly #options: CultMeshProviderRudpTransportOptions;
public constructor(options: CultMeshProviderRudpTransportOptions) {
if (!options.endpoint) throw new Error("CultMesh provider RUDP transport requires endpoint.");
if (!options.runtimeId) throw new Error("CultMesh provider RUDP transport requires runtimeId.");
if (!Number.isInteger(options.connectionId)) throw new Error("CultMesh provider RUDP transport requires integer connectionId.");
this.#options = options;
}
public async connect(
identity: CultMeshProviderIdentity,
signal: AbortSignal,
): Promise<CultMeshProviderConnection> {
if (signal.aborted) throw abortError();
const endpoint = parseEndpoint(this.#options.endpoint);
const socket = this.#options.socketFactory?.() ?? createSocket(endpoint.host.includes(":") ? "udp6" : "udp4");
await bindSocket(socket, this.#options.bindHost ?? (endpoint.host.includes(":") ? "::" : "0.0.0.0"), signal);
const transport = new CultNetRudpSocketTransportConnection({
mode: "client",
runtimeId: this.#options.runtimeId,
transportId: `${this.#options.runtimeId}.cultmesh-provider-session`,
socket,
remoteHost: endpoint.host,
remotePort: endpoint.port,
connectionId: this.#options.connectionId,
maxFragmentBytes: this.#options.maxFragmentBytes ?? 2_048,
});
const peer = new CultNetPeer(transport, { wireContract: "cultnet.schema.v0" });
transport.connect();
try {
await waitUntilConnected(transport, signal, this.#options.connectTimeoutMs ?? 2_000);
return new RudpProviderConnection(identity, peer, transport, this.#options.operationTimeoutMs ?? 5_000);
} catch (error) {
transport.close();
throw error;
}
}
}
class RudpProviderConnection implements CultMeshProviderConnection {
readonly #identity: CultMeshProviderIdentity;
readonly #peer: CultNetPeer;
readonly #transport: CultNetRudpSocketTransportConnection;
readonly #timeoutMs: number;
readonly #listeners = new Set<CultMeshProviderCommandListener>();
readonly #pending = new Map<string, {
operation: string;
payloadSchema: string;
resolve: (response: CultNetOperationResponseMessage) => void;
reject: (error: Error) => void;
timer: NodeJS.Timeout;
}>();
readonly #publications = new Map<string, CultMeshProviderPublication>();
readonly #bufferedCommands = new Map<string, CultMeshProviderCommand>();
#closed = false;
#requestedLeaseDurationMs = 30_000;
public constructor(
identity: CultMeshProviderIdentity,
peer: CultNetPeer,
transport: CultNetRudpSocketTransportConnection,
timeoutMs: number,
) {
this.#identity = identity;
this.#peer = peer;
this.#transport = transport;
this.#timeoutMs = timeoutMs;
peer.on("message", message => this.#onMessage(message));
peer.on("invalidMessage", error => this.#failPending(error));
peer.on("close", () => this.#failPending(new Error("CultMesh provider RUDP connection closed.")));
peer.on("error", error => this.#failPending(error));
}
public async register(
registration: CultMeshProviderRegistration,
signal: AbortSignal,
): Promise<CultMeshProviderLease> {
this.#requestedLeaseDurationMs = registration.requestedLeaseDurationMs;
const response = await this.#operation(
cultMeshProviderSessionOperations.register,
cultMeshProviderSessionSchemas.registration,
cultMeshProviderSessionSchemas.lease,
{ ...registration.identity, requestedLeaseDurationMs: registration.requestedLeaseDurationMs },
signal,
);
const wire = decodeProviderSessionPayload<CultMeshProviderLeaseWire>(response.payload);
assertProviderLease(wire);
assertIdentity(this.#identity, wire);
return { leaseId: wire.leaseId, expiresAt: new Date(wire.expiresAtUtc) };
}
public async renew(lease: CultMeshProviderLease): Promise<CultMeshProviderLease> {
const response = await this.#operation(
cultMeshProviderSessionOperations.renew,
cultMeshProviderSessionSchemas.leaseRenewal,
cultMeshProviderSessionSchemas.lease,
{ leaseId: lease.leaseId, requestedLeaseDurationMs: this.#requestedLeaseDurationMs },
);
const wire = decodeProviderSessionPayload<CultMeshProviderLeaseWire>(response.payload);
assertProviderLease(wire);
assertIdentity(this.#identity, wire);
return { leaseId: wire.leaseId, expiresAt: new Date(wire.expiresAtUtc) };
}
public async publish(
publication: CultMeshProviderPublication,
lease: CultMeshProviderLease,
): Promise<void> {
const document: CultNetRawDocumentRecord = {
schemaId: publication.schemaId,
recordKey: publication.recordKey,
storedAt: new Date().toISOString(),
payloadEncoding: "messagepack",
payload: encode(publication.value),
sourceRuntimeId: this.#identity.serviceInstanceId,
sourceAgentId: this.#identity.providerId,
sourceRole: "provider",
};
await this.#accepted(cultMeshProviderSessionOperations.publicationPut, cultMeshProviderSessionSchemas.publicationPut, {
leaseId: lease.leaseId,
publicationId: publication.publicationId,
document,
});
this.#publications.set(publication.publicationId, publication);
}
public async withdrawPublication(publicationId: string, lease: CultMeshProviderLease): Promise<void> {
const publication = this.#publications.get(publicationId);
if (!publication) throw new Error(`CultMesh provider publication ${publicationId} is not owned by this connection.`);
await this.#accepted(cultMeshProviderSessionOperations.publicationDelete, cultMeshProviderSessionSchemas.publicationDelete, {
leaseId: lease.leaseId,
publicationId,
schemaId: publication.schemaId,
recordKey: publication.recordKey,
});
this.#publications.delete(publicationId);
}
public watchCommands(listener: CultMeshProviderCommandListener): CultMeshProviderUnsubscribe {
this.#listeners.add(listener);
for (const [commandId, command] of this.#bufferedCommands) {
this.#bufferedCommands.delete(commandId);
void Promise.resolve(listener(command)).catch(() => undefined);
}
return () => {
this.#listeners.delete(listener);
};
}
public async publishReceipt(
receipt: CultMeshProviderCommandReceipt,
lease: CultMeshProviderLease,
): Promise<void> {
const wire: CultMeshProviderCommandReceiptWire = {
receiptId: receipt.receiptId,
commandId: receipt.commandId,
commandKind: receipt.commandKind,
providerId: receipt.providerId,
serviceInstanceId: receipt.serviceInstanceId,
state: receipt.state,
completedAtUtc: receipt.completedAt.toISOString(),
result: receipt.result,
error: receipt.error,
};
await this.#accepted(cultMeshProviderSessionOperations.receiptPut, cultMeshProviderSessionSchemas.receiptPut, {
leaseId: lease.leaseId,
receipt: wire,
});
}
public async withdraw(withdrawal: CultMeshProviderWithdrawal): Promise<void> {
await this.#accepted(cultMeshProviderSessionOperations.withdraw, cultMeshProviderSessionSchemas.withdrawal, {
leaseId: withdrawal.leaseId,
});
this.#publications.clear();
}
public close(): void {
if (this.#closed) return;
this.#closed = true;
this.#failPending(new Error("CultMesh provider RUDP connection closed."));
this.#listeners.clear();
this.#transport.close();
}
async #accepted(operation: string, payloadSchema: string, payload: unknown): Promise<CultMeshProviderMutationAcceptanceWire> {
const response = await this.#operation(operation, payloadSchema, cultMeshProviderSessionSchemas.mutationAcceptance, payload);
const acceptance = decodeProviderSessionPayload<CultMeshProviderMutationAcceptanceWire>(response.payload);
assertMutationAcceptance(acceptance);
return acceptance;
}
async #operation(
operation: string,
payloadSchema: string,
responsePayloadSchema: string,
payload: unknown,
signal?: AbortSignal,
): Promise<CultNetOperationResponseMessage> {
if (this.#closed) throw new Error("CultMesh provider RUDP connection is closed.");
if (signal?.aborted) throw abortError();
const messageId = randomUUID();
const request: CultNetOperationRequestMessage = {
schemaVersion: "cultnet.operation_request.v0",
messageId,
serviceId: CULTMESH_PROVIDER_SESSION_SERVICE_ID,
operation,
payloadSchema,
payloadEncoding: "messagepack-base64",
payload: encodeProviderSessionPayload(payload),
sourceRuntimeId: this.#identity.serviceInstanceId,
};
return await new Promise<CultNetOperationResponseMessage>((resolve, reject) => {
const timer = setTimeout(() => {
this.#pending.delete(messageId);
reject(new Error(`CultMesh provider operation ${operation} timed out.`));
}, this.#timeoutMs);
const abort = (): void => {
clearTimeout(timer);
this.#pending.delete(messageId);
reject(abortError());
};
signal?.addEventListener("abort", abort, { once: true });
this.#pending.set(messageId, {
operation,
payloadSchema: responsePayloadSchema,
timer,
resolve: response => {
signal?.removeEventListener("abort", abort);
if (response.serviceId !== CULTMESH_PROVIDER_SESSION_SERVICE_ID || response.operation !== operation) {
reject(new Error(`CultMesh provider response correlation does not match ${operation}.`));
return;
}
if (response.payloadEncoding !== "messagepack-base64") {
reject(new Error(`CultMesh provider response for ${operation} has incompatible payload encoding.`));
return;
}
if (response.status !== "ok") {
reject(new Error(`CultMesh provider operation ${operation} was ${response.status}: ${(response.diagnostics ?? []).join("; ")}`));
return;
}
if (response.payloadSchema !== responsePayloadSchema) {
reject(new Error(`CultMesh provider response for ${operation} has incompatible payload schema.`));
return;
}
resolve(response);
},
reject: error => {
signal?.removeEventListener("abort", abort);
reject(error);
},
});
this.#peer.send(request);
});
}
#onMessage(message: CultNetMessage): void {
if (message.schemaVersion === "cultnet.operation_response.v0") {
const pending = this.#pending.get(message.messageId);
if (!pending) return;
this.#pending.delete(message.messageId);
clearTimeout(pending.timer);
pending.resolve(message);
return;
}
if (message.schemaVersion !== "cultnet.document_put_raw.v0") return;
if (message.document.schemaId !== cultMeshProviderSessionSchemas.command) return;
const wire = decodeProviderCommandDocument(message.document);
if (wire.providerId !== this.#identity.providerId || wire.serviceInstanceId !== this.#identity.serviceInstanceId) return;
const command: CultMeshProviderCommand = { ...wire };
if (this.#listeners.size === 0) {
this.#bufferedCommands.set(command.commandId, command);
return;
}
for (const listener of [...this.#listeners]) void Promise.resolve(listener(command)).catch(() => undefined);
}
#failPending(error: Error): void {
for (const [messageId, pending] of this.#pending) {
this.#pending.delete(messageId);
clearTimeout(pending.timer);
pending.reject(error);
}
}
}
function assertIdentity(identity: CultMeshProviderIdentity, lease: CultMeshProviderLeaseWire): void {
for (const key of ["providerId", "serviceInstanceId", "endpointId", "verseId"] as const) {
if (identity[key] !== lease[key]) throw new Error(`CultMesh provider lease ${key} does not match the registered identity.`);
}
}
function parseEndpoint(endpoint: string): { host: string; port: number } {
const url = new URL(endpoint);
const port = Number(url.port);
if (url.protocol !== "rudp:" || !url.hostname || !Number.isInteger(port) || port <= 0 || port > 65_535) {
throw new Error(`Invalid CultMesh provider RUDP endpoint ${endpoint}.`);
}
return { host: url.hostname, port };
}
function bindSocket(socket: Socket, host: string, signal: AbortSignal): Promise<void> {
return new Promise((resolve, reject) => {
const abort = (): void => {
socket.close();
reject(abortError());
};
signal.addEventListener("abort", abort, { once: true });
socket.once("error", reject);
socket.bind(0, host, () => {
signal.removeEventListener("abort", abort);
socket.removeListener("error", reject);
resolve();
});
});
}
async function waitUntilConnected(
transport: CultNetRudpSocketTransportConnection,
signal: AbortSignal,
timeoutMs: number,
): Promise<void> {
const deadline = Date.now() + timeoutMs;
while (!transport.connected) {
if (signal.aborted) throw abortError();
if (Date.now() >= deadline) throw new Error("CultMesh provider RUDP connection timed out.");
await new Promise(resolve => setTimeout(resolve, 5));
}
}
function abortError(): Error {
const error = new Error("CultMesh provider RUDP operation aborted.");
error.name = "AbortError";
return error;
}

View file

@ -0,0 +1,499 @@
import { encode } from "@msgpack/msgpack";
import type {
CultNetDocumentPutRawMessage,
CultNetOperationRequestMessage,
CultNetOperationResponseMessage,
CultNetRawDocumentRecord,
} from "cultnet-ts";
import type { CultMeshRudpServerSession } from "./index";
import {
CULTMESH_PROVIDER_SESSION_SERVICE_ID,
cultMeshProviderSessionOperations,
cultMeshProviderSessionSchemas,
decodeProviderSessionPayload,
encodeProviderSessionPayload,
type CultMeshProviderCommandReceiptWire,
type CultMeshProviderCommandWire,
type CultMeshProviderLeaseRenewalWire,
type CultMeshProviderLeaseWire,
type CultMeshProviderOperationStatus,
type CultMeshProviderPublicationDeleteWire,
type CultMeshProviderPublicationPutWire,
type CultMeshProviderReceiptPutWire,
type CultMeshProviderRegistrationWire,
type CultMeshProviderWithdrawalWire,
} from "./provider-session-wire";
export interface CultMeshProviderBrokerClock {
now(): Date;
}
export interface CultMeshProviderSessionBrokerOptions {
readonly runtimeId: string;
readonly clock?: CultMeshProviderBrokerClock;
readonly createLeaseId?: () => string;
readonly maximumLeaseDurationMs?: number;
readonly expiryPollMs?: number;
readonly onError?: (error: Error) => void;
readonly authorizeRegistration: (
identity: CultMeshProviderRegistrationWire,
session: CultMeshRudpServerSession,
) => boolean | Promise<boolean>;
readonly acceptPublication: (
identity: CultMeshProviderRegistrationWire,
publicationId: string,
document: CultNetRawDocumentRecord,
) => void | Promise<void>;
readonly deletePublications: (
identity: CultMeshProviderRegistrationWire,
publications: readonly {
publicationId: string;
document: Pick<CultNetRawDocumentRecord, "schemaId" | "recordKey">;
}[],
) => void | Promise<void>;
readonly acceptReceipt: (
identity: CultMeshProviderRegistrationWire,
receipt: CultMeshProviderCommandReceiptWire,
) => void | Promise<void>;
}
interface LeaseRecord {
identity: CultMeshProviderRegistrationWire;
lease: CultMeshProviderLeaseWire;
session?: CultMeshRudpServerSession;
publications: Map<string, CultNetRawDocumentRecord>;
}
export class CultMeshProviderSessionBroker {
readonly #options: CultMeshProviderSessionBrokerOptions;
readonly #clock: CultMeshProviderBrokerClock;
readonly #leases = new Map<string, LeaseRecord>();
readonly #identityLeases = new Map<string, string>();
readonly #sessionLeases = new Map<string, Set<string>>();
readonly #commands = new Map<string, CultMeshProviderCommandWire>();
readonly #receipts = new Map<string, CultMeshProviderCommandReceiptWire>();
readonly #publicationOwners = new Map<string, { identityKey: string; publicationId: string }>();
readonly #expiryTimer: NodeJS.Timeout;
#work: Promise<void> = Promise.resolve();
#leaseSequence = 0;
public constructor(options: CultMeshProviderSessionBrokerOptions) {
if (!options.runtimeId) throw new Error("CultMesh provider-session broker requires runtimeId.");
if (!options.authorizeRegistration) throw new Error("CultMesh provider-session broker requires an explicit registration authority.");
this.#options = options;
this.#clock = options.clock ?? { now: () => new Date() };
this.#expiryTimer = setInterval(() => {
void this.expireLeases().catch(error => {
const normalized = error instanceof Error ? error : new Error(String(error));
if (this.#options.onError) this.#options.onError(normalized);
else console.error(`CultMesh provider-session expiry failed: ${normalized.message}`);
});
}, Math.max(10, options.expiryPollMs ?? 1_000));
this.#expiryTimer.unref?.();
}
public close(): void {
clearInterval(this.#expiryTimer);
}
public async handle(
request: CultNetOperationRequestMessage,
session: CultMeshRudpServerSession,
): Promise<CultNetOperationResponseMessage> {
const work = this.#work.then(() => this.#handle(request, session));
this.#work = work.then(() => undefined, () => undefined);
return await work;
}
async #handle(
request: CultNetOperationRequestMessage,
session: CultMeshRudpServerSession,
): Promise<CultNetOperationResponseMessage> {
if (request.serviceId !== CULTMESH_PROVIDER_SESSION_SERVICE_ID) {
return this.#response(request, "denied", cultMeshProviderSessionSchemas.mutationAcceptance, {}, [
`Unsupported service ${request.serviceId}.`,
]);
}
if (request.payloadEncoding !== "messagepack-base64") {
return this.#response(request, "invalid", cultMeshProviderSessionSchemas.mutationAcceptance, {}, [
"Provider-session operation payloads must use messagepack-base64.",
]);
}
try {
switch (request.operation) {
case cultMeshProviderSessionOperations.register:
return await this.#register(request, session);
case cultMeshProviderSessionOperations.renew:
return await this.#renew(request, session);
case cultMeshProviderSessionOperations.publicationPut:
return await this.#publicationPut(request, session);
case cultMeshProviderSessionOperations.publicationDelete:
return await this.#publicationDelete(request, session);
case cultMeshProviderSessionOperations.receiptPut:
return await this.#receiptPut(request, session);
case cultMeshProviderSessionOperations.withdraw:
return await this.#withdraw(request, session);
default:
return this.#response(request, "invalid", cultMeshProviderSessionSchemas.mutationAcceptance, {}, [
`Unsupported provider-session operation ${request.operation}.`,
]);
}
} catch (error) {
if (error instanceof BrokerInfrastructureError) throw error.cause;
return this.#response(request, error instanceof BrokerStatusError ? error.status : "invalid", cultMeshProviderSessionSchemas.mutationAcceptance, {}, [
error instanceof Error ? error.message : String(error),
]);
}
}
public sessionClosed(session: CultMeshRudpServerSession): void {
for (const leaseId of this.#sessionLeases.get(session.sessionId) ?? []) {
const record = this.#leases.get(leaseId);
if (record?.session?.sessionId === session.sessionId) record.session = undefined;
}
this.#sessionLeases.delete(session.sessionId);
}
public enqueueCommand(command: CultMeshProviderCommandWire): void {
requireText(command.commandId, "commandId");
requireText(command.providerId, "providerId");
requireText(command.serviceInstanceId, "serviceInstanceId");
const completed = this.#receipts.get(command.commandId);
if (completed) {
if (completed.commandKind !== command.commandKind || completed.providerId !== command.providerId || completed.serviceInstanceId !== command.serviceInstanceId) {
throw new Error(`Command id ${command.commandId} is already completed for another command transaction.`);
}
return;
}
const existing = this.#commands.get(command.commandId);
if (existing && !wireEquivalent(existing, command)) {
throw new Error(`Command id ${command.commandId} is already retained for another command transaction.`);
}
this.#commands.set(command.commandId, existing ?? command);
for (const record of this.#leases.values()) {
if (!this.#isActive(record)) continue;
if (record.identity.providerId !== command.providerId || record.identity.serviceInstanceId !== command.serviceInstanceId) continue;
this.#sendCommand(record, command);
}
}
public get activeLeaseCount(): number {
return [...this.#leases.values()].filter(record => this.#isActive(record)).length;
}
public async expireLeases(): Promise<void> {
const work = this.#work.then(() => this.#expireLeases());
this.#work = work.then(() => undefined, () => undefined);
await work;
}
async #expireLeases(): Promise<void> {
const now = this.#clock.now().getTime();
for (const [leaseId, record] of [...this.#leases]) {
if (Date.parse(record.lease.expiresAtUtc) > now) continue;
await this.#options.deletePublications(record.identity, publicationEntries(record));
this.#releasePublicationOwners(record);
this.#removeLease(leaseId, record);
}
}
async #register(request: CultNetOperationRequestMessage, session: CultMeshRudpServerSession): Promise<CultNetOperationResponseMessage> {
expectSchema(request, cultMeshProviderSessionSchemas.registration);
const identity = decodeProviderSessionPayload<CultMeshProviderRegistrationWire>(request.payload);
validateRegistration(identity);
if (!await infrastructure(() => this.#options.authorizeRegistration(identity, session))) {
return this.#response(request, "denied", cultMeshProviderSessionSchemas.mutationAcceptance, {}, [
"The CultNet session does not authorize this provider identity.",
]);
}
const identityKey = identityTuple(identity);
const previousLeaseId = this.#identityLeases.get(identityKey);
let publications = new Map<string, CultNetRawDocumentRecord>();
if (previousLeaseId) {
const previous = this.#leases.get(previousLeaseId);
if (previous) {
publications = previous.publications;
this.#removeLease(previousLeaseId, previous);
}
}
const lease = this.#createLease(identity, identity.requestedLeaseDurationMs);
const record: LeaseRecord = { identity, lease, session, publications };
this.#leases.set(lease.leaseId, record);
this.#identityLeases.set(identityKey, lease.leaseId);
this.#attachSession(session, lease.leaseId);
this.#replayCommands(record);
return this.#response(request, "ok", cultMeshProviderSessionSchemas.lease, lease);
}
async #renew(request: CultNetOperationRequestMessage, session: CultMeshRudpServerSession): Promise<CultNetOperationResponseMessage> {
expectSchema(request, cultMeshProviderSessionSchemas.leaseRenewal);
const renewal = decodeProviderSessionPayload<CultMeshProviderLeaseRenewalWire>(request.payload);
const current = this.#requireLease(renewal.leaseId, session);
const lease = this.#createLease(current.identity, renewal.requestedLeaseDurationMs);
const replacement: LeaseRecord = { ...current, lease, session };
this.#removeLease(renewal.leaseId, current);
this.#leases.set(lease.leaseId, replacement);
this.#identityLeases.set(identityTuple(current.identity), lease.leaseId);
this.#attachSession(session, lease.leaseId);
return this.#response(request, "ok", cultMeshProviderSessionSchemas.lease, lease);
}
async #publicationPut(request: CultNetOperationRequestMessage, session: CultMeshRudpServerSession): Promise<CultNetOperationResponseMessage> {
expectSchema(request, cultMeshProviderSessionSchemas.publicationPut);
const mutation = decodeProviderSessionPayload<CultMeshProviderPublicationPutWire>(request.payload);
const record = this.#requireLease(mutation.leaseId, session);
requireText(mutation.publicationId, "publicationId");
validateDocument(mutation.document);
const previous = record.publications.get(mutation.publicationId);
if (previous && (previous.schemaId !== mutation.document.schemaId || previous.recordKey !== mutation.document.recordKey)) {
return this.#response(request, "conflict", cultMeshProviderSessionSchemas.mutationAcceptance, {}, [
`Publication ${mutation.publicationId} already owns ${previous.schemaId}:${previous.recordKey}. Delete it before changing its tuple.`,
]);
}
const tuple = documentTuple(mutation.document);
const owner = this.#publicationOwners.get(tuple);
const ownerIdentity = identityTuple(record.identity);
if (owner && (owner.identityKey !== ownerIdentity || owner.publicationId !== mutation.publicationId)) {
return this.#response(request, "conflict", cultMeshProviderSessionSchemas.mutationAcceptance, {}, [
`Publication tuple ${mutation.document.schemaId}:${mutation.document.recordKey} is already owned.`,
]);
}
const normalizedDocument: CultNetRawDocumentRecord = {
...mutation.document,
sourceRuntimeId: record.identity.serviceInstanceId,
sourceAgentId: record.identity.providerId,
sourceRole: "provider",
};
await infrastructure(() => this.#options.acceptPublication(record.identity, mutation.publicationId, normalizedDocument));
record.publications.set(mutation.publicationId, normalizedDocument);
this.#publicationOwners.set(tuple, { identityKey: ownerIdentity, publicationId: mutation.publicationId });
return this.#accepted(request, record.lease.leaseId, { publicationId: mutation.publicationId });
}
async #publicationDelete(request: CultNetOperationRequestMessage, session: CultMeshRudpServerSession): Promise<CultNetOperationResponseMessage> {
expectSchema(request, cultMeshProviderSessionSchemas.publicationDelete);
const mutation = decodeProviderSessionPayload<CultMeshProviderPublicationDeleteWire>(request.payload);
const record = this.#requireLease(mutation.leaseId, session);
const owned = record.publications.get(mutation.publicationId);
if (!owned || owned.schemaId !== mutation.schemaId || owned.recordKey !== mutation.recordKey) {
return this.#response(request, "denied", cultMeshProviderSessionSchemas.mutationAcceptance, {}, [
`Publication ${mutation.publicationId} is not owned by this lease with the supplied schema and key.`,
]);
}
await infrastructure(() => this.#options.deletePublications(record.identity, [{ publicationId: mutation.publicationId, document: owned }]));
record.publications.delete(mutation.publicationId);
this.#publicationOwners.delete(documentTuple(owned));
return this.#accepted(request, record.lease.leaseId, { publicationId: mutation.publicationId });
}
async #receiptPut(request: CultNetOperationRequestMessage, session: CultMeshRudpServerSession): Promise<CultNetOperationResponseMessage> {
expectSchema(request, cultMeshProviderSessionSchemas.receiptPut);
const mutation = decodeProviderSessionPayload<CultMeshProviderReceiptPutWire>(request.payload);
const record = this.#requireLease(mutation.leaseId, session);
const receipt = mutation.receipt;
validateReceipt(receipt);
if (receipt.providerId !== record.identity.providerId || receipt.serviceInstanceId !== record.identity.serviceInstanceId) {
return this.#response(request, "denied", cultMeshProviderSessionSchemas.mutationAcceptance, {}, ["Receipt identity does not match its lease."]);
}
const previousReceipt = this.#receipts.get(receipt.commandId);
if (previousReceipt) {
if (!wireEquivalent(previousReceipt, receipt)) {
return this.#response(request, "conflict", cultMeshProviderSessionSchemas.mutationAcceptance, {}, [
`Command ${receipt.commandId} already has a different accepted receipt.`,
]);
}
return this.#accepted(request, record.lease.leaseId, { commandId: receipt.commandId, receiptId: receipt.receiptId });
}
const command = this.#commands.get(receipt.commandId);
if (!command || command.commandKind !== receipt.commandKind || command.providerId !== receipt.providerId || command.serviceInstanceId !== receipt.serviceInstanceId) {
return this.#response(request, "denied", cultMeshProviderSessionSchemas.mutationAcceptance, {}, [
`Receipt ${receipt.receiptId} does not match a retained command for this provider generation.`,
]);
}
await infrastructure(() => this.#options.acceptReceipt(record.identity, receipt));
this.#receipts.set(receipt.commandId, receipt);
this.#commands.delete(receipt.commandId);
return this.#accepted(request, record.lease.leaseId, { commandId: receipt.commandId, receiptId: receipt.receiptId });
}
async #withdraw(request: CultNetOperationRequestMessage, session: CultMeshRudpServerSession): Promise<CultNetOperationResponseMessage> {
expectSchema(request, cultMeshProviderSessionSchemas.withdrawal);
const mutation = decodeProviderSessionPayload<CultMeshProviderWithdrawalWire>(request.payload);
const record = this.#requireLease(mutation.leaseId, session);
await infrastructure(() => this.#options.deletePublications(record.identity, publicationEntries(record)));
this.#releasePublicationOwners(record);
this.#removeLease(mutation.leaseId, record);
return this.#accepted(request, mutation.leaseId);
}
#requireLease(leaseId: string, session: CultMeshRudpServerSession): LeaseRecord {
requireText(leaseId, "leaseId");
const record = this.#leases.get(leaseId);
if (!record || !this.#isActive(record)) throw new BrokerStatusError("expired", `Provider lease ${leaseId} is expired or fenced.`);
if (record.session?.sessionId !== session.sessionId) throw new BrokerStatusError("denied", `Provider lease ${leaseId} belongs to another physical session.`);
return record;
}
#isActive(record: LeaseRecord): boolean {
return Date.parse(record.lease.expiresAtUtc) > this.#clock.now().getTime();
}
#createLease(identity: CultMeshProviderRegistrationWire, requestedMs: number): CultMeshProviderLeaseWire {
if (!Number.isFinite(requestedMs) || requestedMs <= 0) throw new Error("requestedLeaseDurationMs must be positive.");
const durationMs = Math.min(requestedMs, this.#options.maximumLeaseDurationMs ?? 120_000);
const now = this.#clock.now();
return {
providerId: identity.providerId,
serviceInstanceId: identity.serviceInstanceId,
endpointId: identity.endpointId,
verseId: identity.verseId,
leaseId: this.#options.createLeaseId?.() ?? `${this.#options.runtimeId}:lease:${++this.#leaseSequence}`,
validFromUtc: now.toISOString(),
expiresAtUtc: new Date(now.getTime() + durationMs).toISOString(),
};
}
#attachSession(session: CultMeshRudpServerSession, leaseId: string): void {
let leases = this.#sessionLeases.get(session.sessionId);
if (!leases) this.#sessionLeases.set(session.sessionId, leases = new Set());
leases.add(leaseId);
}
#removeLease(leaseId: string, record: LeaseRecord): void {
this.#leases.delete(leaseId);
if (this.#identityLeases.get(identityTuple(record.identity)) === leaseId) this.#identityLeases.delete(identityTuple(record.identity));
this.#sessionLeases.get(record.session?.sessionId ?? "")?.delete(leaseId);
}
#releasePublicationOwners(record: LeaseRecord): void {
const ownerIdentity = identityTuple(record.identity);
for (const [publicationId, document] of record.publications) {
const tuple = documentTuple(document);
const owner = this.#publicationOwners.get(tuple);
if (owner?.identityKey === ownerIdentity && owner.publicationId === publicationId) this.#publicationOwners.delete(tuple);
}
}
#replayCommands(record: LeaseRecord): void {
for (const command of this.#commands.values()) {
if (command.providerId === record.identity.providerId && command.serviceInstanceId === record.identity.serviceInstanceId) this.#sendCommand(record, command);
}
}
#sendCommand(record: LeaseRecord, command: CultMeshProviderCommandWire): void {
if (!record.session) return;
const message: CultNetDocumentPutRawMessage = {
schemaVersion: "cultnet.document_put_raw.v0",
messageId: `${this.#options.runtimeId}:command:${command.commandId}`,
document: {
schemaId: cultMeshProviderSessionSchemas.command,
recordKey: `${command.providerId}/${command.serviceInstanceId}/${command.commandId}`,
storedAt: this.#clock.now().toISOString(),
payloadEncoding: "messagepack",
payload: encode(command),
sourceRuntimeId: this.#options.runtimeId,
sourceRole: "provider-session-broker",
},
};
record.session.send(message);
}
#accepted(request: CultNetOperationRequestMessage, leaseId: string, extra: Record<string, string> = {}): CultNetOperationResponseMessage {
return this.#response(request, "ok", cultMeshProviderSessionSchemas.mutationAcceptance, {
acceptedAtUtc: this.#clock.now().toISOString(),
leaseId,
...extra,
});
}
#response(
request: CultNetOperationRequestMessage,
status: CultMeshProviderOperationStatus,
payloadSchema: string,
payload: unknown,
diagnostics?: string[],
): CultNetOperationResponseMessage {
const response: CultNetOperationResponseMessage = {
schemaVersion: "cultnet.operation_response.v0",
messageId: request.messageId,
serviceId: request.serviceId,
operation: request.operation,
status,
payloadSchema,
payloadEncoding: "messagepack-base64",
payload: encodeProviderSessionPayload(payload),
sourceRuntimeId: this.#options.runtimeId,
};
if (diagnostics) response.diagnostics = diagnostics;
return response;
}
}
class BrokerStatusError extends Error {
public constructor(public readonly status: CultMeshProviderOperationStatus, message: string) {
super(message);
}
}
class BrokerInfrastructureError extends Error {
public constructor(public readonly cause: unknown) {
super(cause instanceof Error ? cause.message : String(cause));
}
}
async function infrastructure<T>(action: () => T | Promise<T>): Promise<T> {
try {
return await action();
} catch (error) {
throw new BrokerInfrastructureError(error);
}
}
function expectSchema(request: CultNetOperationRequestMessage, schema: string): void {
if (request.payloadSchema !== schema) throw new Error(`Operation ${request.operation} requires payload schema ${schema}.`);
}
function validateRegistration(value: CultMeshProviderRegistrationWire): void {
for (const field of ["providerId", "serviceInstanceId", "endpointId", "verseId"] as const) requireText(value[field], field);
if (!Number.isFinite(value.requestedLeaseDurationMs) || value.requestedLeaseDurationMs <= 0) throw new Error("requestedLeaseDurationMs must be positive.");
}
function validateDocument(document: CultNetRawDocumentRecord): void {
requireText(document?.schemaId, "document.schemaId");
requireText(document?.recordKey, "document.recordKey");
if (document.payloadEncoding !== "messagepack" || !(document.payload instanceof Uint8Array)) throw new Error("Provider publications must carry a raw MessagePack document.");
}
function validateReceipt(receipt: CultMeshProviderCommandReceiptWire): void {
requireText(receipt?.receiptId, "receipt.receiptId");
requireText(receipt?.commandId, "receipt.commandId");
requireText(receipt?.commandKind, "receipt.commandKind");
requireText(receipt?.providerId, "receipt.providerId");
requireText(receipt?.serviceInstanceId, "receipt.serviceInstanceId");
if (receipt.state !== "applied" && receipt.state !== "rejected" && receipt.state !== "failed") throw new Error("CultMesh provider-session receipt.state is invalid.");
if (!Number.isFinite(Date.parse(receipt.completedAtUtc))) throw new Error("CultMesh provider-session receipt.completedAtUtc must be RFC3339.");
}
function identityTuple(identity: CultMeshProviderRegistrationWire): string {
return `${identity.verseId}\u0000${identity.providerId}\u0000${identity.serviceInstanceId}\u0000${identity.endpointId}`;
}
function documentTuple(document: Pick<CultNetRawDocumentRecord, "schemaId" | "recordKey">): string {
return `${document.schemaId}\u0000${document.recordKey}`;
}
function wireEquivalent(left: unknown, right: unknown): boolean {
return Buffer.from(encode(left)).equals(Buffer.from(encode(right)));
}
function publicationEntries(record: LeaseRecord): readonly {
publicationId: string;
document: Pick<CultNetRawDocumentRecord, "schemaId" | "recordKey">;
}[] {
return [...record.publications].map(([publicationId, document]) => ({ publicationId, document }));
}
function requireText(value: unknown, name: string): asserts value is string {
if (typeof value !== "string" || value.trim().length === 0) throw new Error(`CultMesh provider-session ${name} must be non-empty.`);
}

View file

@ -0,0 +1,170 @@
import { decode, encode } from "@msgpack/msgpack";
import type { CultNetRawDocumentRecord } from "cultnet-ts";
export const CULTMESH_PROVIDER_SESSION_SERVICE_ID = "gamecult.mesh.provider_session";
export const cultMeshProviderSessionOperations = {
register: "provider.register",
renew: "provider.renew",
publicationPut: "provider.publication.put",
publicationDelete: "provider.publication.delete",
receiptPut: "provider.receipt.put",
withdraw: "provider.withdraw",
} as const;
export const cultMeshProviderSessionSchemas = {
registration: "gamecult.mesh.provider_registration.v1",
lease: "gamecult.mesh.provider_lease.v1",
leaseRenewal: "gamecult.mesh.provider_lease_renewal.v1",
publicationPut: "gamecult.mesh.provider_publication_put.v1",
publicationDelete: "gamecult.mesh.provider_publication_delete.v1",
command: "gamecult.mesh.provider_command.v1",
receiptPut: "gamecult.mesh.provider_receipt_put.v1",
withdrawal: "gamecult.mesh.provider_withdrawal.v1",
mutationAcceptance: "gamecult.mesh.provider_mutation_acceptance.v1",
} as const;
export type CultMeshProviderOperationStatus =
| "ok"
| "conflict"
| "expired"
| "denied"
| "invalid";
export interface CultMeshProviderRegistrationWire {
providerId: string;
serviceInstanceId: string;
endpointId: string;
verseId: string;
requestedLeaseDurationMs: number;
authorityLeaseId?: string;
}
export interface CultMeshProviderLeaseWire {
providerId: string;
serviceInstanceId: string;
endpointId: string;
verseId: string;
leaseId: string;
validFromUtc: string;
expiresAtUtc: string;
}
export interface CultMeshProviderLeaseRenewalWire {
leaseId: string;
requestedLeaseDurationMs: number;
}
export interface CultMeshProviderPublicationPutWire {
leaseId: string;
publicationId: string;
document: CultNetRawDocumentRecord;
}
export interface CultMeshProviderPublicationDeleteWire {
leaseId: string;
publicationId: string;
schemaId: string;
recordKey: string;
}
export interface CultMeshProviderCommandWire {
commandId: string;
commandKind: string;
providerId: string;
serviceInstanceId: string;
payload: unknown;
}
export type CultMeshProviderReceiptStateWire = "applied" | "rejected" | "failed";
export interface CultMeshProviderCommandReceiptWire {
receiptId: string;
commandId: string;
commandKind: string;
providerId: string;
serviceInstanceId: string;
state: CultMeshProviderReceiptStateWire;
completedAtUtc: string;
result?: unknown;
error?: string;
}
export interface CultMeshProviderReceiptPutWire {
leaseId: string;
receipt: CultMeshProviderCommandReceiptWire;
}
export interface CultMeshProviderWithdrawalWire {
leaseId: string;
}
export interface CultMeshProviderMutationAcceptanceWire {
acceptedAtUtc: string;
leaseId?: string;
publicationId?: string;
commandId?: string;
receiptId?: string;
}
export function encodeProviderSessionPayload(value: unknown): string {
return Buffer.from(encode(value)).toString("base64");
}
export function decodeProviderSessionPayload<T>(payload: string): T {
if (typeof payload !== "string" || payload.length === 0) {
throw new Error("CultMesh provider-session payload must be non-empty MessagePack base64.");
}
return decode(Buffer.from(payload, "base64")) as T;
}
export function decodeProviderCommandDocument(document: CultNetRawDocumentRecord): CultMeshProviderCommandWire {
if (document.schemaId !== cultMeshProviderSessionSchemas.command) {
throw new Error(`Expected ${cultMeshProviderSessionSchemas.command}, received ${document.schemaId}.`);
}
if (document.payloadEncoding !== "messagepack") {
throw new Error(`Provider command ${document.recordKey} must use MessagePack.`);
}
const command = decode(document.payload) as Partial<CultMeshProviderCommandWire>;
requireText(command.commandId, "commandId");
requireText(command.commandKind, "commandKind");
requireText(command.providerId, "providerId");
requireText(command.serviceInstanceId, "serviceInstanceId");
return command as CultMeshProviderCommandWire;
}
export function assertProviderLease(value: unknown): asserts value is CultMeshProviderLeaseWire {
const lease = requireObject(value, "provider lease");
requireText(lease.providerId, "providerId");
requireText(lease.serviceInstanceId, "serviceInstanceId");
requireText(lease.endpointId, "endpointId");
requireText(lease.verseId, "verseId");
requireText(lease.leaseId, "leaseId");
requireTimestamp(lease.validFromUtc, "validFromUtc");
requireTimestamp(lease.expiresAtUtc, "expiresAtUtc");
}
export function assertMutationAcceptance(value: unknown): asserts value is CultMeshProviderMutationAcceptanceWire {
const acceptance = requireObject(value, "provider mutation acceptance");
requireTimestamp(acceptance.acceptedAtUtc, "acceptedAtUtc");
}
function requireObject(value: unknown, name: string): Record<string, unknown> {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new Error(`CultMesh ${name} must be a MessagePack map.`);
}
return value as Record<string, unknown>;
}
function requireText(value: unknown, name: string): asserts value is string {
if (typeof value !== "string" || value.trim().length === 0) {
throw new Error(`CultMesh provider-session ${name} must be non-empty.`);
}
}
function requireTimestamp(value: unknown, name: string): asserts value is string {
requireText(value, name);
if (!Number.isFinite(Date.parse(value))) {
throw new Error(`CultMesh provider-session ${name} must be an RFC3339 timestamp.`);
}
}

View file

@ -0,0 +1,161 @@
import test from "node:test";
import assert from "node:assert/strict";
import { decode } from "@msgpack/msgpack";
import { CultNetDocumentRegistry } from "cultnet-ts";
import {
CultMesh,
CultMeshProviderRudpTransport,
CultMeshProviderSessionBroker,
type CultMeshProviderCommand,
type CultMeshProviderIdentity,
type CultMeshProviderPublication,
} from "../src";
const identity: CultMeshProviderIdentity = {
providerId: "voidbot.swarm",
serviceInstanceId: "voidbot-worker-7",
endpointId: "voidbot-worker-7.rudp",
verseId: "voidbot.local",
};
test("RUDP provider transport requires broker acceptance for the full lifecycle", async () => {
const accepted = new Map<string, unknown>();
const acceptedProvenance = new Map<string, { runtime?: string; agent?: string }>();
const deleted: string[] = [];
const receipts: string[] = [];
let leaseSequence = 0;
const broker = new CultMeshProviderSessionBroker({
runtimeId: "odin-test",
createLeaseId: () => `lease-${++leaseSequence}`,
authorizeRegistration: () => true,
acceptPublication: (_identity, publicationId, document) => {
accepted.set(publicationId, decode(document.payload));
acceptedProvenance.set(publicationId, {
runtime: document.sourceRuntimeId,
agent: document.sourceAgentId,
});
},
deletePublications: (_identity, publications) => {
for (const { publicationId } of publications) {
deleted.push(publicationId);
accepted.delete(publicationId);
}
},
acceptReceipt: (_identity, receipt) => {
receipts.push(receipt.receiptId);
},
});
const server = CultMesh.createRudpDocumentServer("odin-test", 0x43554c54, {
bindPort: 0,
documents: new CultNetDocumentRegistry(),
onOperationRequest: (request, session) => broker.handle(request, session),
onSessionClosed: session => broker.sessionClosed(session),
});
await server.start();
const transport = new CultMeshProviderRudpTransport({
endpoint: `rudp://127.0.0.1:${server.bind.port}`,
runtimeId: identity.serviceInstanceId,
connectionId: 0x43554c54,
});
const connection = await transport.connect(identity, new AbortController().signal);
const secondIdentity: CultMeshProviderIdentity = {
providerId: "aetheria",
serviceInstanceId: "aetheria-daemon-2",
endpointId: "aetheria-daemon-2.rudp",
verseId: "aetheria.local",
};
const secondConnection = await new CultMeshProviderRudpTransport({
endpoint: `rudp://127.0.0.1:${server.bind.port}`,
runtimeId: secondIdentity.serviceInstanceId,
connectionId: 0x43554c54,
}).connect(secondIdentity, new AbortController().signal);
try {
broker.enqueueCommand({
commandId: "refresh-1",
commandKind: "voidbot.refresh",
providerId: identity.providerId,
serviceInstanceId: identity.serviceInstanceId,
payload: { force: true },
});
const lease = await connection.register({ identity, requestedLeaseDurationMs: 10_000 }, new AbortController().signal);
assert.equal(lease.leaseId, "lease-1");
const secondLease = await secondConnection.register({
identity: secondIdentity,
requestedLeaseDurationMs: 10_000,
}, new AbortController().signal);
assert.equal(secondLease.leaseId, "lease-2");
assert.equal(broker.activeLeaseCount, 2, "the broker demultiplexes simultaneous physical provider sessions");
const publication: CultMeshProviderPublication = {
publicationId: "voidbot.surface",
documentType: "gamecult.eve.surface_state",
schemaId: "gamecult.eve.surface_state.v1",
recordKey: "voidbot.swarm",
value: { generation: 3 },
};
await connection.publish(publication, lease);
assert.deepEqual(accepted.get(publication.publicationId), publication.value);
assert.deepEqual(acceptedProvenance.get(publication.publicationId), {
runtime: identity.serviceInstanceId,
agent: identity.providerId,
}, "the broker derives provenance from the fenced identity");
const secondPublication: CultMeshProviderPublication = {
publicationId: "aetheria.surface",
documentType: "gamecult.eve.surface_state",
schemaId: "gamecult.eve.surface_state.v1",
recordKey: "aetheria.pilot",
value: { generation: 8 },
};
await secondConnection.publish(secondPublication, secondLease);
await assert.rejects(secondConnection.publish({
...secondPublication,
publicationId: "aetheria.stolen-tuple",
recordKey: publication.recordKey,
}, secondLease), /already owned/);
await assert.rejects(
secondConnection.publish({ ...secondPublication, value: { generation: 9 } }, lease),
/belongs to another physical session/,
);
const commandArrived = deferred<CultMeshProviderCommand>();
connection.watchCommands(command => commandArrived.resolve(command));
const command = await commandArrived.promise;
assert.equal(command.commandId, "refresh-1");
await connection.publishReceipt({
receiptId: "receipt-refresh-1",
commandId: command.commandId,
commandKind: command.commandKind,
providerId: identity.providerId,
serviceInstanceId: identity.serviceInstanceId,
state: "applied",
completedAt: new Date(),
result: { refreshed: true },
}, lease);
assert.deepEqual(receipts, ["receipt-refresh-1"]);
const renewed = await connection.renew(lease);
assert.equal(renewed.leaseId, "lease-3");
await assert.rejects(connection.withdrawPublication(publication.publicationId, lease), /expired|fenced/);
await connection.withdraw({ identity, leaseId: renewed.leaseId, publicationIds: [publication.publicationId] });
await secondConnection.withdraw({
identity: secondIdentity,
leaseId: secondLease.leaseId,
publicationIds: [secondPublication.publicationId],
});
assert.deepEqual(deleted, [publication.publicationId, secondPublication.publicationId]);
assert.equal(accepted.size, 0);
assert.equal(broker.activeLeaseCount, 0);
} finally {
connection.close();
secondConnection.close();
server.close();
broker.close();
}
});
function deferred<T>(): { promise: Promise<T>; resolve(value: T): void } {
let resolve!: (value: T) => void;
return { promise: new Promise<T>(done => resolve = done), resolve };
}

View file

@ -0,0 +1,17 @@
import test from "node:test";
import assert from "node:assert/strict";
import { encodeProviderSessionPayload } from "../src/provider-session-wire";
const REGISTRATION_GOLDEN = "hqpwcm92aWRlcklkqGFldGhlcmlhsXNlcnZpY2VJbnN0YW5jZUlkq2FldGhlcmlhLTQyqmVuZHBvaW50SWSvYWV0aGVyaWEtcHVibGljp3ZlcnNlSWSmcHVibGljuHJlcXVlc3RlZExlYXNlRHVyYXRpb25Nc811MLBhdXRob3JpdHlMZWFzZUlkq2F1dGhvcml0eS03";
test("provider registration matches the shared C# MessagePack golden payload", () => {
assert.equal(encodeProviderSessionPayload({
providerId: "aetheria",
serviceInstanceId: "aetheria-42",
endpointId: "aetheria-public",
verseId: "public",
requestedLeaseDurationMs: 30_000,
authorityLeaseId: "authority-7",
}), REGISTRATION_GOLDEN);
});

View file

@ -0,0 +1,487 @@
using System;
using System.Globalization;
using GameCult.Networking;
using MessagePack;
namespace GameCult.Mesh
{
/// <summary>
/// Canonical CultNet service, operation, payload-schema, and status identifiers for
/// the CultMesh provider-session control plane.
/// </summary>
public static class CultMeshProviderSessionWireContract
{
/// <summary>The CultNet service id.</summary>
public const string ServiceId = "gamecult.mesh.provider_session";
/// <summary>The registration operation.</summary>
public const string RegisterOperation = "provider.register";
/// <summary>The lease-renewal operation.</summary>
public const string RenewOperation = "provider.renew";
/// <summary>The publication-put operation.</summary>
public const string PublicationPutOperation = "provider.publication.put";
/// <summary>The publication-delete operation.</summary>
public const string PublicationDeleteOperation = "provider.publication.delete";
/// <summary>The command-receipt-put operation.</summary>
public const string ReceiptPutOperation = "provider.receipt.put";
/// <summary>The provider-withdrawal operation.</summary>
public const string WithdrawOperation = "provider.withdraw";
/// <summary>The registration payload schema.</summary>
public const string RegistrationSchema = "gamecult.mesh.provider_registration.v1";
/// <summary>The provider-lease payload schema.</summary>
public const string LeaseSchema = "gamecult.mesh.provider_lease.v1";
/// <summary>The lease-renewal payload schema.</summary>
public const string LeaseRenewalSchema = "gamecult.mesh.provider_lease_renewal.v1";
/// <summary>The publication-put payload schema.</summary>
public const string PublicationPutSchema = "gamecult.mesh.provider_publication_put.v1";
/// <summary>The publication-delete payload schema.</summary>
public const string PublicationDeleteSchema = "gamecult.mesh.provider_publication_delete.v1";
/// <summary>The broker-to-provider command document schema.</summary>
public const string CommandSchema = "gamecult.mesh.provider_command.v1";
/// <summary>The command-receipt-put payload schema.</summary>
public const string ReceiptPutSchema = "gamecult.mesh.provider_receipt_put.v1";
/// <summary>The provider-withdrawal payload schema.</summary>
public const string WithdrawalSchema = "gamecult.mesh.provider_withdrawal.v1";
/// <summary>The accepted-mutation response payload schema.</summary>
public const string MutationAcceptanceSchema = "gamecult.mesh.provider_mutation_acceptance.v1";
/// <summary>The successful application status.</summary>
public const string OkStatus = "ok";
/// <summary>The identity or generation conflict status.</summary>
public const string ConflictStatus = "conflict";
/// <summary>The expired-lease status.</summary>
public const string ExpiredStatus = "expired";
/// <summary>The authority-denied status.</summary>
public const string DeniedStatus = "denied";
/// <summary>The invalid-request status.</summary>
public const string InvalidStatus = "invalid";
/// <summary>The CultNet inner-payload encoding.</summary>
public const string PayloadEncoding = "messagepack-base64";
}
/// <summary>Requests a fenced provider lease from the provider-session broker.</summary>
[MessagePackObject]
public sealed class CultMeshProviderRegistrationWire
{
/// <summary>Gets or sets the stable provider identity.</summary>
[Key("providerId")] public string ProviderId { get; set; } = string.Empty;
/// <summary>Gets or sets the provider process generation.</summary>
[Key("serviceInstanceId")] public string ServiceInstanceId { get; set; } = string.Empty;
/// <summary>Gets or sets the advertised endpoint identity.</summary>
[Key("endpointId")] public string EndpointId { get; set; } = string.Empty;
/// <summary>Gets or sets the Verse identity.</summary>
[Key("verseId")] public string VerseId { get; set; } = string.Empty;
/// <summary>Gets or sets the requested lease duration in milliseconds.</summary>
[Key("requestedLeaseDurationMs")] public int RequestedLeaseDurationMs { get; set; }
/// <summary>Gets or sets the optional authority lease authorizing registration.</summary>
[Key("authorityLeaseId")] public string? AuthorityLeaseId { get; set; }
}
/// <summary>Identifies the provider generation authorized by a broker lease.</summary>
[MessagePackObject]
public sealed class CultMeshProviderLeaseWire
{
/// <summary>Gets or sets the stable provider identity.</summary>
[Key("providerId")] public string ProviderId { get; set; } = string.Empty;
/// <summary>Gets or sets the authorized provider process generation.</summary>
[Key("serviceInstanceId")] public string ServiceInstanceId { get; set; } = string.Empty;
/// <summary>Gets or sets the authorized endpoint identity.</summary>
[Key("endpointId")] public string EndpointId { get; set; } = string.Empty;
/// <summary>Gets or sets the authorized Verse identity.</summary>
[Key("verseId")] public string VerseId { get; set; } = string.Empty;
/// <summary>Gets or sets the unique fencing lease id.</summary>
[Key("leaseId")] public string LeaseId { get; set; } = string.Empty;
/// <summary>Gets or sets the RFC3339 lease validity start.</summary>
[Key("validFromUtc")] public string ValidFromUtc { get; set; } = string.Empty;
/// <summary>Gets or sets the RFC3339 lease expiry.</summary>
[Key("expiresAtUtc")] public string ExpiresAtUtc { get; set; } = string.Empty;
}
/// <summary>Requests replacement of an active provider lease.</summary>
[MessagePackObject]
public sealed class CultMeshProviderLeaseRenewalWire
{
/// <summary>Gets or sets the lease being replaced.</summary>
[Key("leaseId")] public string LeaseId { get; set; } = string.Empty;
/// <summary>Gets or sets the requested replacement duration in milliseconds.</summary>
[Key("requestedLeaseDurationMs")] public int RequestedLeaseDurationMs { get; set; }
}
/// <summary>Publishes one typed document under an active provider lease.</summary>
[MessagePackObject]
public sealed class CultMeshProviderPublicationPutWire
{
/// <summary>Gets or sets the active provider lease.</summary>
[Key("leaseId")] public string LeaseId { get; set; } = string.Empty;
/// <summary>Gets or sets the provider-owned publication identity.</summary>
[Key("publicationId")] public string PublicationId { get; set; } = string.Empty;
/// <summary>Gets or sets the exact typed CultNet document.</summary>
[Key("document")] public CultNetRawDocumentRecord Document { get; set; } = new CultNetRawDocumentRecord();
}
/// <summary>Deletes one exact provider-owned publication tuple.</summary>
[MessagePackObject]
public sealed class CultMeshProviderPublicationDeleteWire
{
/// <summary>Gets or sets the active provider lease.</summary>
[Key("leaseId")] public string LeaseId { get; set; } = string.Empty;
/// <summary>Gets or sets the provider-owned publication identity.</summary>
[Key("publicationId")] public string PublicationId { get; set; } = string.Empty;
/// <summary>Gets or sets the exact published schema id.</summary>
[Key("schemaId")] public string SchemaId { get; set; } = string.Empty;
/// <summary>Gets or sets the exact published record key.</summary>
[Key("recordKey")] public string RecordKey { get; set; } = string.Empty;
}
/// <summary>A retained command routed from the broker to one provider generation.</summary>
[MessagePackObject]
public sealed class CultMeshProviderCommandWire
{
/// <summary>Gets or sets the stable command identity.</summary>
[Key("commandId")] public string CommandId { get; set; } = string.Empty;
/// <summary>Gets or sets the provider-defined command kind.</summary>
[Key("commandKind")] public string CommandKind { get; set; } = string.Empty;
/// <summary>Gets or sets the target provider identity.</summary>
[Key("providerId")] public string ProviderId { get; set; } = string.Empty;
/// <summary>Gets or sets the target provider process generation.</summary>
[Key("serviceInstanceId")] public string ServiceInstanceId { get; set; } = string.Empty;
/// <summary>Gets or sets the typed command payload value.</summary>
[Key("payload")] public object? Payload { get; set; }
}
/// <summary>The exactly-once outcome of one provider command.</summary>
[MessagePackObject]
public sealed class CultMeshProviderCommandReceiptWire
{
/// <summary>Gets or sets the stable receipt identity.</summary>
[Key("receiptId")] public string ReceiptId { get; set; } = string.Empty;
/// <summary>Gets or sets the completed command identity.</summary>
[Key("commandId")] public string CommandId { get; set; } = string.Empty;
/// <summary>Gets or sets the completed command kind.</summary>
[Key("commandKind")] public string CommandKind { get; set; } = string.Empty;
/// <summary>Gets or sets the provider identity.</summary>
[Key("providerId")] public string ProviderId { get; set; } = string.Empty;
/// <summary>Gets or sets the provider process generation.</summary>
[Key("serviceInstanceId")] public string ServiceInstanceId { get; set; } = string.Empty;
/// <summary>Gets or sets applied, rejected, or failed.</summary>
[Key("state")] public string State { get; set; } = string.Empty;
/// <summary>Gets or sets the RFC3339 completion time.</summary>
[Key("completedAtUtc")] public string CompletedAtUtc { get; set; } = string.Empty;
/// <summary>Gets or sets the optional typed result value.</summary>
[Key("result")] public object? Result { get; set; }
/// <summary>Gets or sets the optional bounded failure detail.</summary>
[Key("error")] public string? Error { get; set; }
}
/// <summary>Submits a durable command receipt under an active provider lease.</summary>
[MessagePackObject]
public sealed class CultMeshProviderReceiptPutWire
{
/// <summary>Gets or sets the active provider lease.</summary>
[Key("leaseId")] public string LeaseId { get; set; } = string.Empty;
/// <summary>Gets or sets the durable command receipt.</summary>
[Key("receipt")] public CultMeshProviderCommandReceiptWire Receipt { get; set; } = new CultMeshProviderCommandReceiptWire();
}
/// <summary>Withdraws the provider generation identified by a lease.</summary>
[MessagePackObject]
public sealed class CultMeshProviderWithdrawalWire
{
/// <summary>Gets or sets the lease whose owned publications must be withdrawn.</summary>
[Key("leaseId")] public string LeaseId { get; set; } = string.Empty;
}
/// <summary>Confirms broker acceptance of one provider-session mutation.</summary>
[MessagePackObject]
public sealed class CultMeshProviderMutationAcceptanceWire
{
/// <summary>Gets or sets the RFC3339 broker acceptance time.</summary>
[Key("acceptedAtUtc")] public string AcceptedAtUtc { get; set; } = string.Empty;
/// <summary>Gets or sets the accepted lease identity, when applicable.</summary>
[Key("leaseId")] public string? LeaseId { get; set; }
/// <summary>Gets or sets the accepted publication identity, when applicable.</summary>
[Key("publicationId")] public string? PublicationId { get; set; }
/// <summary>Gets or sets the accepted command identity, when applicable.</summary>
[Key("commandId")] public string? CommandId { get; set; }
/// <summary>Gets or sets the accepted receipt identity, when applicable.</summary>
[Key("receiptId")] public string? ReceiptId { get; set; }
}
/// <summary>
/// Encodes provider-session payloads inside the existing CultNet operation envelope.
/// Transport acknowledgement is deliberately not treated as application acceptance.
/// </summary>
public static class CultMeshProviderSessionWire
{
private static readonly MessagePackSerializerOptions Options =
MessagePackSerializerOptions.Standard.WithSecurity(MessagePackSecurity.UntrustedData);
/// <summary>Encodes one validated provider-session value as MessagePack base64.</summary>
public static string EncodePayload<T>(T value)
{
if (value == null) throw new ArgumentNullException(nameof(value));
Validate(value);
return Convert.ToBase64String(MessagePackSerializer.Serialize(value, Options));
}
/// <summary>Decodes and validates one MessagePack-base64 provider-session value.</summary>
public static T DecodePayload<T>(string payload)
{
RequireText(payload, nameof(payload));
byte[] bytes;
try
{
bytes = Convert.FromBase64String(payload);
}
catch (FormatException exception)
{
throw new ArgumentException("Provider-session payload must be MessagePack base64.", nameof(payload), exception);
}
var value = MessagePackSerializer.Deserialize<T>(bytes, Options);
if (value == null) throw new MessagePackSerializationException("Provider-session payload decoded to null.");
Validate(value);
return value;
}
/// <summary>Decodes a retained provider command carried as a raw CultNet document.</summary>
public static CultMeshProviderCommandWire DecodeCommandDocument(CultNetRawDocumentRecord document)
{
if (document == null) throw new ArgumentNullException(nameof(document));
if (!string.Equals(document.SchemaId, CultMeshProviderSessionWireContract.CommandSchema, StringComparison.Ordinal))
throw new ArgumentException($"Expected provider command schema '{CultMeshProviderSessionWireContract.CommandSchema}'.", nameof(document));
if (!string.Equals(document.PayloadEncoding, "messagepack", StringComparison.Ordinal))
throw new ArgumentException("Provider command document must use MessagePack.", nameof(document));
var command = MessagePackSerializer.Deserialize<CultMeshProviderCommandWire>(document.Payload, Options);
if (command == null) throw new MessagePackSerializationException("Provider command decoded to null.");
Validate(command);
return command;
}
/// <summary>Creates a canonical provider-session CultNet operation request.</summary>
public static CultNetOperationRequestMessage CreateRequest<T>(
string messageId,
string operation,
string payloadSchema,
T payload,
string? sourceRuntimeId = null,
string? targetRuntimeId = null)
{
RequireText(messageId, nameof(messageId));
ValidateOperation(operation, payloadSchema);
return new CultNetOperationRequestMessage
{
MessageId = messageId,
ServiceId = CultMeshProviderSessionWireContract.ServiceId,
Operation = operation,
PayloadSchema = payloadSchema,
PayloadEncoding = CultMeshProviderSessionWireContract.PayloadEncoding,
Payload = EncodePayload(payload),
SourceRuntimeId = sourceRuntimeId,
TargetRuntimeId = targetRuntimeId
};
}
/// <summary>Validates an operation request envelope and decodes its typed payload.</summary>
public static T DecodeRequest<T>(
CultNetOperationRequestMessage request,
string expectedOperation,
string expectedPayloadSchema)
{
if (request == null) throw new ArgumentNullException(nameof(request));
RequireEnvelope(request.ServiceId, request.Operation, request.PayloadSchema, request.PayloadEncoding,
expectedOperation, expectedPayloadSchema);
return DecodePayload<T>(request.Payload);
}
/// <summary>Creates a correlated application-level operation response.</summary>
public static CultNetOperationResponseMessage CreateResponse<T>(
CultNetOperationRequestMessage request,
string status,
string payloadSchema,
T payload,
string? sourceRuntimeId = null)
{
if (request == null) throw new ArgumentNullException(nameof(request));
ValidateStatus(status);
return new CultNetOperationResponseMessage
{
MessageId = RequireText(request.MessageId, nameof(request.MessageId)),
ServiceId = CultMeshProviderSessionWireContract.ServiceId,
Operation = RequireText(request.Operation, nameof(request.Operation)),
Status = status,
PayloadSchema = RequireText(payloadSchema, nameof(payloadSchema)),
PayloadEncoding = CultMeshProviderSessionWireContract.PayloadEncoding,
Payload = EncodePayload(payload),
SourceRuntimeId = sourceRuntimeId
};
}
/// <summary>Validates an operation response envelope and decodes its typed payload.</summary>
public static T DecodeResponse<T>(
CultNetOperationResponseMessage response,
string expectedOperation,
string expectedPayloadSchema)
{
if (response == null) throw new ArgumentNullException(nameof(response));
ValidateStatus(response.Status);
RequireEnvelope(response.ServiceId, response.Operation, response.PayloadSchema, response.PayloadEncoding,
expectedOperation, expectedPayloadSchema);
return DecodePayload<T>(response.Payload);
}
/// <summary>Validates the invariants of a known provider-session wire value.</summary>
public static void Validate<T>(T value)
{
switch (value)
{
case CultMeshProviderRegistrationWire registration:
RequireIdentity(registration.ProviderId, registration.ServiceInstanceId, registration.EndpointId, registration.VerseId);
RequirePositive(registration.RequestedLeaseDurationMs, nameof(registration.RequestedLeaseDurationMs));
RequireOptionalText(registration.AuthorityLeaseId, nameof(registration.AuthorityLeaseId));
break;
case CultMeshProviderLeaseWire lease:
RequireIdentity(lease.ProviderId, lease.ServiceInstanceId, lease.EndpointId, lease.VerseId);
RequireText(lease.LeaseId, nameof(lease.LeaseId));
var validFrom = RequireTimestamp(lease.ValidFromUtc, nameof(lease.ValidFromUtc));
var expiresAt = RequireTimestamp(lease.ExpiresAtUtc, nameof(lease.ExpiresAtUtc));
if (expiresAt <= validFrom) throw new ArgumentException("Provider lease must expire after validFromUtc.");
break;
case CultMeshProviderLeaseRenewalWire renewal:
RequireText(renewal.LeaseId, nameof(renewal.LeaseId));
RequirePositive(renewal.RequestedLeaseDurationMs, nameof(renewal.RequestedLeaseDurationMs));
break;
case CultMeshProviderPublicationPutWire publication:
RequireText(publication.LeaseId, nameof(publication.LeaseId));
RequireText(publication.PublicationId, nameof(publication.PublicationId));
if (publication.Document == null) throw new ArgumentException("Provider publication document is required.");
RequireText(publication.Document.SchemaId, nameof(publication.Document.SchemaId));
RequireText(publication.Document.RecordKey, nameof(publication.Document.RecordKey));
if (!string.Equals(publication.Document.PayloadEncoding, "messagepack", StringComparison.Ordinal))
throw new ArgumentException("Provider publication document must use MessagePack.");
break;
case CultMeshProviderPublicationDeleteWire deletion:
RequireText(deletion.LeaseId, nameof(deletion.LeaseId));
RequireText(deletion.PublicationId, nameof(deletion.PublicationId));
RequireText(deletion.SchemaId, nameof(deletion.SchemaId));
RequireText(deletion.RecordKey, nameof(deletion.RecordKey));
break;
case CultMeshProviderCommandWire command:
RequireText(command.CommandId, nameof(command.CommandId));
RequireText(command.CommandKind, nameof(command.CommandKind));
RequireText(command.ProviderId, nameof(command.ProviderId));
RequireText(command.ServiceInstanceId, nameof(command.ServiceInstanceId));
break;
case CultMeshProviderCommandReceiptWire receipt:
RequireText(receipt.ReceiptId, nameof(receipt.ReceiptId));
RequireText(receipt.CommandId, nameof(receipt.CommandId));
RequireText(receipt.CommandKind, nameof(receipt.CommandKind));
RequireText(receipt.ProviderId, nameof(receipt.ProviderId));
RequireText(receipt.ServiceInstanceId, nameof(receipt.ServiceInstanceId));
ValidateReceiptState(receipt.State);
RequireTimestamp(receipt.CompletedAtUtc, nameof(receipt.CompletedAtUtc));
RequireOptionalText(receipt.Error, nameof(receipt.Error));
break;
case CultMeshProviderReceiptPutWire receiptPut:
RequireText(receiptPut.LeaseId, nameof(receiptPut.LeaseId));
if (receiptPut.Receipt == null) throw new ArgumentException("Provider receipt is required.");
Validate(receiptPut.Receipt);
break;
case CultMeshProviderWithdrawalWire withdrawal:
RequireText(withdrawal.LeaseId, nameof(withdrawal.LeaseId));
break;
case CultMeshProviderMutationAcceptanceWire acceptance:
RequireTimestamp(acceptance.AcceptedAtUtc, nameof(acceptance.AcceptedAtUtc));
RequireOptionalText(acceptance.LeaseId, nameof(acceptance.LeaseId));
RequireOptionalText(acceptance.PublicationId, nameof(acceptance.PublicationId));
RequireOptionalText(acceptance.CommandId, nameof(acceptance.CommandId));
RequireOptionalText(acceptance.ReceiptId, nameof(acceptance.ReceiptId));
break;
}
}
private static void RequireEnvelope(
string serviceId,
string operation,
string payloadSchema,
string payloadEncoding,
string expectedOperation,
string expectedPayloadSchema)
{
if (!string.Equals(serviceId, CultMeshProviderSessionWireContract.ServiceId, StringComparison.Ordinal))
throw new ArgumentException($"Expected provider-session service '{CultMeshProviderSessionWireContract.ServiceId}'.");
if (!string.Equals(operation, expectedOperation, StringComparison.Ordinal))
throw new ArgumentException($"Expected provider-session operation '{expectedOperation}'.");
if (!string.Equals(payloadSchema, expectedPayloadSchema, StringComparison.Ordinal))
throw new ArgumentException($"Expected provider-session payload schema '{expectedPayloadSchema}'.");
if (!string.Equals(payloadEncoding, CultMeshProviderSessionWireContract.PayloadEncoding, StringComparison.Ordinal))
throw new ArgumentException("Provider-session operation payload must use MessagePack base64.");
}
private static void ValidateOperation(string operation, string payloadSchema)
{
var expectedSchema = operation switch
{
CultMeshProviderSessionWireContract.RegisterOperation => CultMeshProviderSessionWireContract.RegistrationSchema,
CultMeshProviderSessionWireContract.RenewOperation => CultMeshProviderSessionWireContract.LeaseRenewalSchema,
CultMeshProviderSessionWireContract.PublicationPutOperation => CultMeshProviderSessionWireContract.PublicationPutSchema,
CultMeshProviderSessionWireContract.PublicationDeleteOperation => CultMeshProviderSessionWireContract.PublicationDeleteSchema,
CultMeshProviderSessionWireContract.ReceiptPutOperation => CultMeshProviderSessionWireContract.ReceiptPutSchema,
CultMeshProviderSessionWireContract.WithdrawOperation => CultMeshProviderSessionWireContract.WithdrawalSchema,
_ => throw new ArgumentException($"Unsupported provider-session operation '{operation}'.", nameof(operation))
};
if (!string.Equals(payloadSchema, expectedSchema, StringComparison.Ordinal))
throw new ArgumentException($"Operation '{operation}' requires payload schema '{expectedSchema}'.", nameof(payloadSchema));
}
private static void ValidateStatus(string status)
{
if (status != CultMeshProviderSessionWireContract.OkStatus &&
status != CultMeshProviderSessionWireContract.ConflictStatus &&
status != CultMeshProviderSessionWireContract.ExpiredStatus &&
status != CultMeshProviderSessionWireContract.DeniedStatus &&
status != CultMeshProviderSessionWireContract.InvalidStatus)
throw new ArgumentException($"Unsupported provider-session status '{status}'.", nameof(status));
}
private static void ValidateReceiptState(string state)
{
if (state != "applied" && state != "rejected" && state != "failed")
throw new ArgumentException($"Unsupported provider receipt state '{state}'.", nameof(state));
}
private static void RequireIdentity(string providerId, string serviceInstanceId, string endpointId, string verseId)
{
RequireText(providerId, nameof(providerId));
RequireText(serviceInstanceId, nameof(serviceInstanceId));
RequireText(endpointId, nameof(endpointId));
RequireText(verseId, nameof(verseId));
}
private static string RequireText(string value, string name)
{
if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException("Value must be non-empty.", name);
return value;
}
private static void RequireOptionalText(string? value, string name)
{
if (value != null && string.IsNullOrWhiteSpace(value))
throw new ArgumentException("Optional value must be null or non-empty.", name);
}
private static void RequirePositive(int value, string name)
{
if (value <= 0) throw new ArgumentOutOfRangeException(name, "Value must be positive.");
}
private static DateTimeOffset RequireTimestamp(string value, string name)
{
RequireText(value, name);
if (!DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var timestamp))
throw new ArgumentException("Value must be an RFC3339 timestamp.", name);
return timestamp;
}
}
}

View file

@ -0,0 +1,224 @@
using System;
using System.Collections.Generic;
using GameCult.Networking;
using MessagePack;
using NUnit.Framework;
namespace GameCult.Mesh.Tests;
[TestFixture]
public sealed class CultMeshProviderSessionWireTests
{
private const string RegistrationGolden = "hqpwcm92aWRlcklkqGFldGhlcmlhsXNlcnZpY2VJbnN0YW5jZUlkq2FldGhlcmlhLTQyqmVuZHBvaW50SWSvYWV0aGVyaWEtcHVibGljp3ZlcnNlSWSmcHVibGljuHJlcXVlc3RlZExlYXNlRHVyYXRpb25Nc811MLBhdXRob3JpdHlMZWFzZUlkq2F1dGhvcml0eS03";
[Test]
public void Registration_RoundTrips_InsideCanonicalCultNetOperationEnvelope()
{
var registration = new CultMeshProviderRegistrationWire
{
ProviderId = "aetheria",
ServiceInstanceId = "aetheria-42",
EndpointId = "aetheria-public",
VerseId = "public",
RequestedLeaseDurationMs = 30_000,
AuthorityLeaseId = "authority-7"
};
var request = CultMeshProviderSessionWire.CreateRequest(
"register-1",
CultMeshProviderSessionWireContract.RegisterOperation,
CultMeshProviderSessionWireContract.RegistrationSchema,
registration,
sourceRuntimeId: "aetheria-42",
targetRuntimeId: "odin");
var envelopeBytes = CultNetSchemaMessageSerialization.Serialize(request);
var decodedEnvelope = (CultNetOperationRequestMessage)CultNetSchemaMessageSerialization.Deserialize(envelopeBytes);
var decoded = CultMeshProviderSessionWire.DecodeRequest<CultMeshProviderRegistrationWire>(
decodedEnvelope,
CultMeshProviderSessionWireContract.RegisterOperation,
CultMeshProviderSessionWireContract.RegistrationSchema);
Assert.Multiple(() =>
{
Assert.That(CultMeshProviderSessionWire.EncodePayload(registration), Is.EqualTo(RegistrationGolden),
"C# and TypeScript must emit the same canonical registration payload");
Assert.That(decodedEnvelope.ServiceId, Is.EqualTo("gamecult.mesh.provider_session"));
Assert.That(decodedEnvelope.PayloadEncoding, Is.EqualTo("messagepack-base64"));
Assert.That(decoded.ProviderId, Is.EqualTo("aetheria"));
Assert.That(decoded.ServiceInstanceId, Is.EqualTo("aetheria-42"));
Assert.That(decoded.EndpointId, Is.EqualTo("aetheria-public"));
Assert.That(decoded.VerseId, Is.EqualTo("public"));
Assert.That(decoded.RequestedLeaseDurationMs, Is.EqualTo(30_000));
Assert.That(decoded.AuthorityLeaseId, Is.EqualTo("authority-7"));
});
}
[Test]
public void PublicationPut_RoundTrips_ExactRawDocumentTuple()
{
var publication = new CultMeshProviderPublicationPutWire
{
LeaseId = "lease-1",
PublicationId = "surface:pilot",
Document = new CultNetRawDocumentRecord
{
SchemaId = "gamecult.eve.surface.v1",
RecordKey = "eve:surface:pilot",
StoredAt = "2026-07-14T12:00:00Z",
PayloadEncoding = "messagepack",
Payload = [0x81, 0xA2, 0x69, 0x64, 0x01],
SourceRuntimeId = "aetheria-42"
}
};
var encoded = CultMeshProviderSessionWire.EncodePayload(publication);
var decoded = CultMeshProviderSessionWire.DecodePayload<CultMeshProviderPublicationPutWire>(encoded);
Assert.Multiple(() =>
{
Assert.That(decoded.LeaseId, Is.EqualTo("lease-1"));
Assert.That(decoded.PublicationId, Is.EqualTo("surface:pilot"));
Assert.That(decoded.Document.SchemaId, Is.EqualTo("gamecult.eve.surface.v1"));
Assert.That(decoded.Document.RecordKey, Is.EqualTo("eve:surface:pilot"));
Assert.That(decoded.Document.Payload, Is.EqualTo(publication.Document.Payload));
});
}
[Test]
public void CommandAndReceipt_UseCamelCaseMessagePackMaps()
{
var command = new CultMeshProviderCommandWire
{
CommandId = "cmd-7",
CommandKind = "pilot.thrust",
ProviderId = "aetheria",
ServiceInstanceId = "aetheria-42",
Payload = new Dictionary<string, object> { ["axis"] = 0.75 }
};
var receiptPut = new CultMeshProviderReceiptPutWire
{
LeaseId = "lease-1",
Receipt = new CultMeshProviderCommandReceiptWire
{
ReceiptId = "receipt-7",
CommandId = command.CommandId,
CommandKind = command.CommandKind,
ProviderId = command.ProviderId,
ServiceInstanceId = command.ServiceInstanceId,
State = "applied",
CompletedAtUtc = "2026-07-14T12:01:02.345Z",
Result = new Dictionary<string, object> { ["accepted"] = true }
}
};
var encodedCommand = CultMeshProviderSessionWire.EncodePayload(command);
var encodedReceipt = CultMeshProviderSessionWire.EncodePayload(receiptPut);
var decodedCommand = CultMeshProviderSessionWire.DecodePayload<CultMeshProviderCommandWire>(encodedCommand);
var decodedReceipt = CultMeshProviderSessionWire.DecodePayload<CultMeshProviderReceiptPutWire>(encodedReceipt);
var decodedCommandDocument = CultMeshProviderSessionWire.DecodeCommandDocument(new CultNetRawDocumentRecord
{
SchemaId = CultMeshProviderSessionWireContract.CommandSchema,
RecordKey = "provider-command:aetheria:aetheria-42:cmd-7",
PayloadEncoding = "messagepack",
Payload = Convert.FromBase64String(encodedCommand)
});
var commandJson = MessagePackSerializer.ConvertToJson(Convert.FromBase64String(encodedCommand));
var receiptJson = MessagePackSerializer.ConvertToJson(Convert.FromBase64String(encodedReceipt));
Assert.Multiple(() =>
{
Assert.That(commandJson, Does.Contain("\"commandId\""));
Assert.That(commandJson, Does.Contain("\"commandKind\""));
Assert.That(commandJson, Does.Contain("\"serviceInstanceId\""));
Assert.That(commandJson, Does.Contain("\"payload\""));
Assert.That(decodedCommand.CommandKind, Is.EqualTo("pilot.thrust"));
Assert.That(decodedCommand.Payload, Is.Not.Null);
Assert.That(decodedCommandDocument.CommandId, Is.EqualTo("cmd-7"));
Assert.That(receiptJson, Does.Contain("\"completedAtUtc\""));
Assert.That(receiptJson, Does.Contain("\"receiptId\""));
Assert.That(receiptJson, Does.Contain("\"result\""));
Assert.That(decodedReceipt.Receipt.State, Is.EqualTo("applied"));
Assert.That(decodedReceipt.Receipt.Result, Is.Not.Null);
});
}
[Test]
public void LeaseResponse_RoundTrips_WithApplicationAcceptance()
{
var request = CultMeshProviderSessionWire.CreateRequest(
"register-1",
CultMeshProviderSessionWireContract.RegisterOperation,
CultMeshProviderSessionWireContract.RegistrationSchema,
new CultMeshProviderRegistrationWire
{
ProviderId = "aetheria",
ServiceInstanceId = "aetheria-42",
EndpointId = "public",
VerseId = "verse",
RequestedLeaseDurationMs = 10_000
});
var response = CultMeshProviderSessionWire.CreateResponse(
request,
CultMeshProviderSessionWireContract.OkStatus,
CultMeshProviderSessionWireContract.LeaseSchema,
new CultMeshProviderLeaseWire
{
ProviderId = "aetheria",
ServiceInstanceId = "aetheria-42",
EndpointId = "public",
VerseId = "verse",
LeaseId = "lease-2",
ValidFromUtc = "2026-07-14T12:00:00Z",
ExpiresAtUtc = "2026-07-14T12:00:10Z"
},
sourceRuntimeId: "odin");
var lease = CultMeshProviderSessionWire.DecodeResponse<CultMeshProviderLeaseWire>(
response,
CultMeshProviderSessionWireContract.RegisterOperation,
CultMeshProviderSessionWireContract.LeaseSchema);
Assert.That(response.Status, Is.EqualTo("ok"));
Assert.That(lease.LeaseId, Is.EqualTo("lease-2"));
Assert.That(lease.ExpiresAtUtc, Is.EqualTo("2026-07-14T12:00:10Z"));
}
[Test]
public void Validation_RejectsSplitAuthorityAndMalformedLifecycleState()
{
Assert.Multiple(() =>
{
Assert.That(
() => CultMeshProviderSessionWire.CreateRequest(
"bad-schema",
CultMeshProviderSessionWireContract.RegisterOperation,
CultMeshProviderSessionWireContract.PublicationPutSchema,
new CultMeshProviderRegistrationWire()),
Throws.ArgumentException);
Assert.That(
() => CultMeshProviderSessionWire.EncodePayload(new CultMeshProviderLeaseWire
{
ProviderId = "provider",
ServiceInstanceId = "instance",
EndpointId = "endpoint",
VerseId = "verse",
LeaseId = "lease",
ValidFromUtc = "2026-07-14T12:00:10Z",
ExpiresAtUtc = "2026-07-14T12:00:00Z"
}),
Throws.ArgumentException);
Assert.That(
() => CultMeshProviderSessionWire.EncodePayload(new CultMeshProviderCommandReceiptWire
{
ReceiptId = "receipt",
CommandId = "command",
CommandKind = "kind",
ProviderId = "provider",
ServiceInstanceId = "instance",
State = "maybe",
CompletedAtUtc = "2026-07-14T12:00:00Z"
}),
Throws.ArgumentException);
});
}
}