cultnet-ts/content: the TypeScript CDN manifest, codec, hashing, record keys and packer

One browser-safe module owns the manifest and chunk-ref shapes, their MessagePack form (positional ten-array, metadata as
ordered entries, duplicate keys and unsafe integers refused, payload depth through cultcache-ts's own checkPayloadDepth),
normalizeHash/parseSha256Hex, both record-key grammars (StableToken per UTF-16 unit), WebCrypto sha256Hex and packContent.
cultcache-ts/document re-exports checkPayloadDepth. scripts/write-cultmesh-content-vectors.mjs writes the TypeScript
manifest vectors for the C# reference to judge.

Builds. Co-Authored-By below.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-19 14:35:15 +02:00
commit 0ca81f52da
6 changed files with 667 additions and 1 deletions

View file

@ -0,0 +1,99 @@
{
"vectors": [
{
"label": "ts multi-chunk body",
"kind": "manifest",
"input": {
"artifactId": "Pixel Heroes/Hüt ☃",
"kind": "asset",
"version": "",
"mimeType": "application/x-cultnet-ts-vector",
"createdAtUtc": "2026-10-19T00:00:00.0000000+00:00",
"chunkSizeBytes": 131072,
"tags": [
"a",
"b"
],
"metadata": [
[
"10",
"x"
],
[
"alpha",
"y"
],
[
"2",
"z"
]
],
"body": {
"length": 777777,
"multiplier": 7,
"modulus": 256
}
},
"recordKey": "mesh:cdn:artifact:pixel-heroes-hüt:unversioned:cf8a453a36981321977355448d24df49bcefee22be6e3730b9140e87c29013f9",
"messagePackBase64": "mrVQaXhlbCBIZXJvZXMvSMO8dCDimIOlYXNzZXSg2UBjZjhhNDUzYTM2OTgxMzIxOTc3MzU1NDQ4ZDI0ZGY0OWJjZWZlZTIyYmU2ZTM3MzBiOTE0MGU4N2MyOTAxM2Y5zgAL3jG/YXBwbGljYXRpb24veC1jdWx0bmV0LXRzLXZlY3RvctkhMjAyNi0xMC0xOVQwMDowMDowMC4wMDAwMDAwKzAwOjAwlpTZQDM3N2M3YjBjYzIyOWY0NzA1NjcwZDYzZDU0NGMyNzViNjYzZGY2ZThjODMxYjA5ODQ2YTVjNDhjY2EwOGI4NDgAzgACAADZT21lc2g6Y2RuOmNodW5rOjM3N2M3YjBjYzIyOWY0NzA1NjcwZDYzZDU0NGMyNzViNjYzZGY2ZThjODMxYjA5ODQ2YTVjNDhjY2EwOGI4NDiU2UAzNzdjN2IwY2MyMjlmNDcwNTY3MGQ2M2Q1NDRjMjc1YjY2M2RmNmU4YzgzMWIwOTg0NmE1YzQ4Y2NhMDhiODQ4zgACAADOAAIAANlPbWVzaDpjZG46Y2h1bms6Mzc3YzdiMGNjMjI5ZjQ3MDU2NzBkNjNkNTQ0YzI3NWI2NjNkZjZlOGM4MzFiMDk4NDZhNWM0OGNjYTA4Yjg0OJTZQDM3N2M3YjBjYzIyOWY0NzA1NjcwZDYzZDU0NGMyNzViNjYzZGY2ZThjODMxYjA5ODQ2YTVjNDhjY2EwOGI4NDjOAAQAAM4AAgAA2U9tZXNoOmNkbjpjaHVuazozNzdjN2IwY2MyMjlmNDcwNTY3MGQ2M2Q1NDRjMjc1YjY2M2RmNmU4YzgzMWIwOTg0NmE1YzQ4Y2NhMDhiODQ4lNlAMzc3YzdiMGNjMjI5ZjQ3MDU2NzBkNjNkNTQ0YzI3NWI2NjNkZjZlOGM4MzFiMDk4NDZhNWM0OGNjYTA4Yjg0OM4ABgAAzgACAADZT21lc2g6Y2RuOmNodW5rOjM3N2M3YjBjYzIyOWY0NzA1NjcwZDYzZDU0NGMyNzViNjYzZGY2ZThjODMxYjA5ODQ2YTVjNDhjY2EwOGI4NDiU2UAzNzdjN2IwY2MyMjlmNDcwNTY3MGQ2M2Q1NDRjMjc1YjY2M2RmNmU4YzgzMWIwOTg0NmE1YzQ4Y2NhMDhiODQ4zgAIAADOAAIAANlPbWVzaDpjZG46Y2h1bms6Mzc3YzdiMGNjMjI5ZjQ3MDU2NzBkNjNkNTQ0YzI3NWI2NjNkZjZlOGM4MzFiMDk4NDZhNWM0OGNjYTA4Yjg0OJTZQGEwYmI2OWY0NDg2MWViM2RiNTYwMjZhMWFiYmY5MWZmYTYyMTMyNTA4ZDVhMDZlMGQwZGRjNWNjMjZiZTMzZmPOAAoAAM4AAd4x2U9tZXNoOmNkbjpjaHVuazphMGJiNjlmNDQ4NjFlYjNkYjU2MDI2YTFhYmJmOTFmZmE2MjEzMjUwOGQ1YTA2ZTBkMGRkYzVjYzI2YmUzM2ZjkqFhoWKDojEwoXilYWxwaGGheaEyoXo="
},
{
"label": "ts empty body",
"kind": "manifest",
"input": {
"artifactId": "Pixel Heroes/Hüt ☃",
"kind": "asset",
"version": "",
"mimeType": "application/x-cultnet-ts-vector",
"createdAtUtc": "2026-10-19T00:00:00.0000000+00:00",
"chunkSizeBytes": 131072,
"tags": [
"a",
"b"
],
"metadata": [
[
"10",
"x"
],
[
"alpha",
"y"
],
[
"2",
"z"
]
],
"body": {
"length": 0,
"multiplier": 7,
"modulus": 256
}
},
"recordKey": "mesh:cdn:artifact:pixel-heroes-hüt:unversioned:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"messagePackBase64": "mrVQaXhlbCBIZXJvZXMvSMO8dCDimIOlYXNzZXSg2UBlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1AL9hcHBsaWNhdGlvbi94LWN1bHRuZXQtdHMtdmVjdG9y2SEyMDI2LTEwLTE5VDAwOjAwOjAwLjAwMDAwMDArMDA6MDCRlNlAZTNiMGM0NDI5OGZjMWMxNDlhZmJmNGM4OTk2ZmI5MjQyN2FlNDFlNDY0OWI5MzRjYTQ5NTk5MWI3ODUyYjg1NQAA2U9tZXNoOmNkbjpjaHVuazplM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1kqFhoWKDojEwoXilYWxwaGGheaEyoXo="
},
{
"label": "ts single chunk, versioned",
"kind": "manifest",
"input": {
"artifactId": "Pixel Heroes/Hüt ☃",
"kind": "asset",
"version": "V1 Beta",
"mimeType": "application/x-cultnet-ts-vector",
"createdAtUtc": "2026-10-19T00:00:00.0000000+00:00",
"chunkSizeBytes": 4194304,
"tags": [],
"metadata": [],
"body": {
"length": 1000,
"multiplier": 7,
"modulus": 256
}
},
"recordKey": "mesh:cdn:artifact:pixel-heroes-hüt:v1-beta:89f4ff56a25dd1db06a4ce6033603775d705fb96f30f8693733fef602a1ca532",
"messagePackBase64": "mrVQaXhlbCBIZXJvZXMvSMO8dCDimIOlYXNzZXSnVjEgQmV0YdlAODlmNGZmNTZhMjVkZDFkYjA2YTRjZTYwMzM2MDM3NzVkNzA1ZmI5NmYzMGY4NjkzNzMzZmVmNjAyYTFjYTUzMs0D6L9hcHBsaWNhdGlvbi94LWN1bHRuZXQtdHMtdmVjdG9y2SEyMDI2LTEwLTE5VDAwOjAwOjAwLjAwMDAwMDArMDA6MDCRlNlAODlmNGZmNTZhMjVkZDFkYjA2YTRjZTYwMzM2MDM3NzVkNzA1ZmI5NmYzMGY4NjkzNzMzZmVmNjAyYTFjYTUzMgDNA+jZT21lc2g6Y2RuOmNodW5rOjg5ZjRmZjU2YTI1ZGQxZGIwNmE0Y2U2MDMzNjAzNzc1ZDcwNWZiOTZmMzBmODY5MzczM2ZlZjYwMmExY2E1MzKQgA=="
}
]
}

View file

@ -105,7 +105,7 @@ export function documentPayloadBytes(payload: DocumentPayload): Uint8Array {
return payload as unknown as Uint8Array;
}
export { CultCachePayloadDepthError, MAX_PAYLOAD_DEPTH } from "./payload-depth";
export { CultCachePayloadDepthError, MAX_PAYLOAD_DEPTH, checkPayloadDepth } from "./payload-depth";
// The payload nesting bound covers the MessagePack the library itself writes: the plain default formatter and a codec. A
// user formatter's bytes are opaque and in any format; its format and bounds are its own.

View file

@ -29,6 +29,11 @@
"types": "./dist/cultmesh-authority.d.ts",
"require": "./dist/cultmesh-authority.js",
"default": "./dist/cultmesh-authority.js"
},
"./content": {
"types": "./dist/content.d.ts",
"require": "./dist/content.js",
"default": "./dist/content.js"
}
},
"files": [

View file

@ -0,0 +1,212 @@
// The one TypeScript owner of the CultMesh CDN shapes: the manifest and chunk reference, their MessagePack form, hash
// normalization, both record-key grammars, SHA-256 hashing and packing. The C# reference is
// `src/GameCult.Mesh/CultMeshCdn.cs`; `contracts/cultmesh/content-vectors.*.json` and `cdn-record-key-vectors.json` pin
// the bytes and the keys.
//
// Browser-safe by construction: `crypto.subtle`, `TextEncoder`, `Uint8Array` and the codecs this package already ships.
// No `node:` import. A manifest names its chunks by hash and carries no address: where bytes are fetched from is the
// caller's to resolve through CultMesh.
import { Decoder, encode } from "@msgpack/msgpack";
import { checkPayloadDepth } from "@gamecult/cultcache-ts/document";
import { isNullOrWhiteSpaceCSharp, trimCSharp } from "./cultmesh-authority";
export const CULTMESH_CDN_MANIFEST_SCHEMA = "gamecult.mesh.cdn_artifact_manifest.v1";
export const DEFAULT_CHUNK_SIZE_BYTES = 4 * 1024 * 1024;
export const EMPTY_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
export interface CultMeshCdnChunkRef {
readonly chunkHash: string;
readonly offset: number;
readonly sizeBytes: number;
readonly recordKey: string;
}
// Metadata is ordered entries, never an object: a JS object reorders integer-like keys, and the entry order is wire bytes.
export interface CultMeshCdnArtifactManifest {
readonly artifactId: string;
readonly kind: string;
readonly version: string;
readonly contentHash: string;
readonly sizeBytes: number;
readonly mimeType: string;
readonly createdAtUtc: string;
readonly chunks: readonly CultMeshCdnChunkRef[];
readonly tags: readonly string[];
readonly metadata: readonly (readonly [string, string])[];
}
export interface CultMeshCdnPackOptions {
readonly kind?: string;
readonly version?: string;
readonly mimeType?: string;
readonly createdAtUtc?: string;
readonly chunkSizeBytes?: number;
readonly tags?: readonly string[];
readonly metadata?: readonly (readonly [string, string])[];
}
export interface CultMeshCdnPackedChunk {
readonly chunkHash: string;
readonly payload: Uint8Array;
}
// ---- hashes and keys -------------------------------------------------------------------------------------------------
// `ToLowerInvariant`: each character on its own, so no context rule applies (a final sigma stays `σ`), and a character
// whose lower case is not a single character (`İ`) stays as it is.
const lowerInvariant = (text: string): string =>
Array.from(text, character => { const lower = character.toLowerCase(); return Array.from(lower).length === 1 ? lower : character; }).join("");
/** `CultMeshCdn.NormalizeHash`: a case-insensitive `sha256:` prefix is stripped before the trim, and nothing checks for hex. */
export function normalizeHash(hash: string, parameterName = "hash"): string {
if (isNullOrWhiteSpaceCSharp(hash)) throw new Error(`Hash must be non-empty. (Parameter '${parameterName}')`);
return lowerInvariant(trimCSharp(hash.slice(0, 7).toLowerCase() === "sha256:" ? hash.slice(7) : hash));
}
/** The hash a file name or a fetch may rest on: `normalizeHash`, then exactly 64 lowercase hexadecimal digits. */
export function parseSha256Hex(hash: string, parameterName = "hash"): string {
const hex = normalizeHash(hash, parameterName);
if (!/^[0-9a-f]{64}$/.test(hex)) throw new Error(`Hash must be 64 hexadecimal digits. (Parameter '${parameterName}')`);
return hex;
}
export async function sha256Hex(bytes: Uint8Array): Promise<string> {
const subtle = globalThis.crypto?.subtle;
if (!subtle) throw new Error("WebCrypto SHA-256 is unavailable.");
return Array.from(new Uint8Array(await subtle.digest("SHA-256", bytes as BufferSource)), byte => byte.toString(16).padStart(2, "0")).join("");
}
export const chunkRecordKey = (chunkHash: string): string => `mesh:cdn:chunk:${normalizeHash(chunkHash, "chunkHash")}`;
// `CultMeshCdn.StableToken`, unit by unit: a letter or decimal digit, `-`, `_` and `.` survive, anything else (a lone
// surrogate included) becomes `-`, and a token that is empty after the trim falls back to the hash of the original.
async function stableToken(value: string, parameterName: string): Promise<string> {
if (isNullOrWhiteSpaceCSharp(value)) throw new Error(`Value must be non-empty. (Parameter '${parameterName}')`);
const token = lowerInvariant(trimCSharp(value)).split("").map(unit => /^[\p{L}\p{Nd}\-_.]$/u.test(unit) ? unit : "-").join("").replace(/^-+|-+$/g, "");
return token || sha256Hex(new TextEncoder().encode(value));
}
export async function artifactRecordKey(artifactId: string, version: string, contentHash: string): Promise<string> {
const artifact = await stableToken(artifactId, "artifactId");
const versioned = isNullOrWhiteSpaceCSharp(version) ? "unversioned" : await stableToken(version, "version");
return `mesh:cdn:artifact:${artifact}:${versioned}:${normalizeHash(contentHash, "contentHash")}`;
}
// ---- validation ------------------------------------------------------------------------------------------------------
/** `ValidateManifestShape`, in its order and with its messages, then the metadata keys a dictionary could not hold twice. */
export function validateManifest(manifest: CultMeshCdnArtifactManifest): void {
if (manifest.sizeBytes < 0) throw new Error("CDN artifact manifest has a negative size.");
let expectedOffset = 0;
for (const reference of [...manifest.chunks].sort((a, b) => a.offset - b.offset)) {
if (reference.offset !== expectedOffset) throw new Error("CDN artifact chunks are not contiguous.");
if (reference.sizeBytes < 0) throw new Error("CDN artifact chunk has a negative size.");
expectedOffset += reference.sizeBytes;
}
if (expectedOffset !== manifest.sizeBytes) throw new Error("CDN artifact chunk sizes do not sum to the manifest size.");
if (new Set(manifest.metadata.map(([key]) => key)).size !== manifest.metadata.length) {
throw new Error("CDN artifact manifest metadata has a duplicate key.");
}
}
// ---- MessagePack -----------------------------------------------------------------------------------------------------
const concat = (parts: readonly Uint8Array[]): Uint8Array => {
const bytes = new Uint8Array(parts.reduce((sum, part) => sum + part.length, 0));
parts.reduce((offset, part) => (bytes.set(part, offset), offset + part.length), 0);
return bytes;
};
const mapHeader = (count: number): Uint8Array =>
count < 16 ? Uint8Array.of(0x80 | count)
: count < 0x10000 ? Uint8Array.of(0xde, count >> 8, count & 0xff)
: Uint8Array.of(0xdf, count >>> 24, (count >> 16) & 0xff, (count >> 8) & 0xff, count & 0xff);
/** The positional ten-array of the C# `[MessagePackObject]`: chunk references are four-arrays, metadata a map in entry order. */
export function encodeManifest(manifest: CultMeshCdnArtifactManifest): Uint8Array {
const { artifactId, kind, version, contentHash, sizeBytes, mimeType, createdAtUtc, chunks, tags, metadata } = manifest;
return concat([
Uint8Array.of(0x9a),
...[artifactId, kind, version, contentHash, sizeBytes, mimeType, createdAtUtc].map(field => encode(field)),
encode(chunks.map(c => [c.chunkHash, c.offset, c.sizeBytes, c.recordKey])),
encode(tags),
mapHeader(metadata.length),
...metadata.flatMap(([key, value]) => [encode(key), encode(value)]),
]);
}
const wrongType = (field: string): never => { throw new Error(`CDN artifact manifest field '${field}' has the wrong type.`); };
const text = (value: unknown, field: string): string => typeof value === "string" ? value : wrongType(field);
const list = (value: unknown, field: string): unknown[] => Array.isArray(value) ? value : wrongType(field);
const record = (value: unknown, field: string): Record<string, unknown> =>
typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Uint8Array) ? value as Record<string, unknown> : wrongType(field);
const SAFE = BigInt(Number.MAX_SAFE_INTEGER);
// Any integer width, as the reference reads; a value a JS number cannot hold exactly is refused, never rounded.
const integer = (value: unknown, field: string): number => {
const number = typeof value === "bigint" ? (value >= -SAFE && value <= SAFE ? Number(value) : NaN) : value;
return typeof number === "number" && Number.isSafeInteger(number) ? number : wrongType(field);
};
/** The reference's reader: depth first, exact types, then `validateManifest`. */
export function decodeManifest(bytes: Uint8Array): CultMeshCdnArtifactManifest {
checkPayloadDepth(bytes, "CDN artifact manifest");
// The decoder builds a JS object, which reorders integer-like keys; the converter sees the keys in wire order.
const keys: string[] = [];
const decoder = new Decoder({ useBigInt64: true, mapKeyConverter: key => { keys.push(text(key, "metadata")); return key as string; } });
const fields = list(decoder.decode(bytes), "manifest");
if (fields.length !== 10) wrongType("manifest");
const map = record(fields[9], "metadata");
const manifest: CultMeshCdnArtifactManifest = {
artifactId: text(fields[0], "artifactId"),
kind: text(fields[1], "kind"),
version: text(fields[2], "version"),
contentHash: text(fields[3], "contentHash"),
sizeBytes: integer(fields[4], "sizeBytes"),
mimeType: text(fields[5], "mimeType"),
createdAtUtc: text(fields[6], "createdAtUtc"),
chunks: list(fields[7], "chunks").map(item => {
const chunk = list(item, "chunks");
if (chunk.length !== 4) wrongType("chunks");
const sizeBytes = integer(chunk[2], "chunks.sizeBytes");
return { chunkHash: text(chunk[0], "chunks.chunkHash"), offset: integer(chunk[1], "chunks.offset"),
sizeBytes: sizeBytes >= -(2 ** 31) && sizeBytes < 2 ** 31 ? sizeBytes : wrongType("chunks.sizeBytes"), recordKey: text(chunk[3], "chunks.recordKey") };
}),
tags: list(fields[8], "tags").map(tag => text(tag, "tags")),
metadata: keys.map(key => [key, text(map[key], "metadata")] as const),
};
validateManifest(manifest);
return manifest;
}
// ---- packing ---------------------------------------------------------------------------------------------------------
/** `CultMeshCdn.PackArtifact`: content-addressed chunks and the manifest that names them. An empty body is one empty chunk. */
export async function packContent(
artifactId: string,
bytes: Uint8Array,
options: CultMeshCdnPackOptions = {},
): Promise<{ manifest: CultMeshCdnArtifactManifest; chunks: CultMeshCdnPackedChunk[] }> {
if (isNullOrWhiteSpaceCSharp(artifactId)) throw new Error("Artifact id must be non-empty. (Parameter 'artifactId')");
const size = options.chunkSizeBytes ?? DEFAULT_CHUNK_SIZE_BYTES;
if (!Number.isInteger(size) || size < 1 || size > 2 ** 31 - 1) throw new Error("ChunkSizeBytes must be between 1 and 2147483647.");
const offsets = bytes.length === 0 ? [0] : Array.from({ length: Math.ceil(bytes.length / size) }, (_, index) => index * size);
const chunks = await Promise.all(offsets.map(async offset => {
const payload = bytes.slice(offset, offset + size);
return { chunkHash: await sha256Hex(payload), payload };
}));
const manifest: CultMeshCdnArtifactManifest = {
artifactId,
kind: isNullOrWhiteSpaceCSharp(options.kind) ? "asset" : options.kind!,
version: options.version ?? "",
contentHash: await sha256Hex(bytes),
sizeBytes: bytes.length,
mimeType: isNullOrWhiteSpaceCSharp(options.mimeType) ? "application/octet-stream" : options.mimeType!,
createdAtUtc: options.createdAtUtc ?? `${new Date().toISOString().slice(0, 23)}0000+00:00`,
chunks: chunks.map(({ chunkHash, payload }, index) => ({ chunkHash, offset: offsets[index]!, sizeBytes: payload.length, recordKey: chunkRecordKey(chunkHash) })),
tags: options.tags ?? [],
metadata: options.metadata ?? [],
};
validateManifest(manifest);
return { manifest, chunks };
}

View file

@ -0,0 +1,290 @@
// The shared CultMesh CDN shapes, pinned rule by rule.
//
// Wire bytes and record keys are judged against files the C# reference and Rust write
// (`contracts/cultmesh/content-vectors.cs-written.json`, `.rs-written.json`, `cdn-record-key-vectors.json`); a fixture
// authored on this side alone would only agree with itself. The TypeScript-written file is judged here from its own
// declared inputs and by the C# reference in `CultMeshContentVectorTests`.
import test from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { build } from "esbuild";
import { encode } from "@msgpack/msgpack";
import { CultCachePayloadDepthError } from "@gamecult/cultcache-ts/document";
import {
EMPTY_SHA256,
artifactRecordKey,
chunkRecordKey,
decodeManifest,
encodeManifest,
normalizeHash,
packContent,
parseSha256Hex,
sha256Hex,
validateManifest,
type CultMeshCdnArtifactManifest,
} from "../src/content";
const CONTRACTS = join(__dirname, "..", "..", "..", "..", "contracts", "cultmesh");
const vectors = (name: string): { vectors: { label: string; kind: string; messagePackBase64: string }[] } =>
JSON.parse(readFileSync(join(CONTRACTS, name), "utf8"));
const manifestVectors = (name: string) => vectors(name).vectors.filter(v => v.kind === "manifest")
.map(v => ({ label: v.label, bytes: new Uint8Array(Buffer.from(v.messagePackBase64, "base64")) }));
const csManifest = (label: string) => manifestVectors("content-vectors.cs-written.json").find(v => v.label === label)!.bytes;
// The C# fixture: byte i is i % 251, 262,144-byte chunks, and the options its test passes.
const FIXTURE_BODY = Uint8Array.from({ length: 1_315_551 }, (_, i) => i % 251);
const FIXTURE_OPTIONS = {
chunkSizeBytes: 262_144,
version: "1",
mimeType: "application/x-cultnet-vector",
createdAtUtc: "2026-09-22T00:00:00.0000000+00:00",
};
const FIXTURE_HASH = "f4ae2a7c95f4d966058f75c13185aa0ab6a91fc5afad886296ef2aee755a2af1";
const HASH = "a".repeat(64);
const manifestOf = (over: Partial<CultMeshCdnArtifactManifest> = {}): CultMeshCdnArtifactManifest => ({
artifactId: "id", kind: "asset", version: "1", contentHash: HASH, sizeBytes: 4, mimeType: "text/plain", createdAtUtc: "t",
chunks: [{ chunkHash: HASH, offset: 0, sizeBytes: 4, recordKey: `mesh:cdn:chunk:${HASH}` }], tags: ["t"], metadata: [["k", "v"]],
...over,
});
// Wire bytes of a ten-array with any field replaced, metadata as a plain object (no integer-like keys).
const wire = (over: Record<number, unknown> = {}) => {
const m = manifestOf();
const fields: unknown[] = [m.artifactId, m.kind, m.version, m.contentHash, m.sizeBytes, m.mimeType, m.createdAtUtc,
m.chunks.map(c => [c.chunkHash, c.offset, c.sizeBytes, c.recordKey]), m.tags, { k: "v" }];
for (const [index, value] of Object.entries(over)) fields[Number(index)] = value;
return encode(fields, { useBigInt64: true, maxDepth: 1000 });
};
const refusal = (bytes: Uint8Array) => assert.throws(() => decodeManifest(bytes), /./);
test("each C# manifest vector decodes and re-encodes byte-identically", () => {
const vs = manifestVectors("content-vectors.cs-written.json");
assert.deepEqual(vs.map(v => v.label), ["manifest", "manifest_out_of_order", "manifest_metadata_order", "manifest_empty_body"]);
for (const { label, bytes } of vs) assert.deepEqual(encodeManifest(decodeManifest(bytes)), bytes, label);
});
test("the Rust-written manifests decode and re-encode byte-identically", () => {
const vs = manifestVectors("content-vectors.rs-written.json");
assert.ok(vs.length >= 4);
for (const { label, bytes } of vs) assert.deepEqual(encodeManifest(decodeManifest(bytes)), bytes, label);
});
test("packContent of the C# fixture body and options equals the cs-written manifest, and of an empty body manifest_empty_body", async () => {
const { manifest, chunks } = await packContent("eureka/content-vector", FIXTURE_BODY, FIXTURE_OPTIONS);
assert.deepEqual(encodeManifest(manifest), csManifest("manifest"));
assert.equal(manifest.contentHash, FIXTURE_HASH);
assert.equal(chunks.length, 6);
const empty = await packContent("eureka/content-vector", new Uint8Array(0), FIXTURE_OPTIONS);
assert.deepEqual(encodeManifest(empty.manifest), csManifest("manifest_empty_body"));
assert.equal(empty.chunks.length, 1, "an empty artifact packs one chunk");
assert.equal(empty.chunks[0]!.payload.length, 0);
assert.equal(empty.chunks[0]!.chunkHash, EMPTY_SHA256);
});
test("the packed chunks are copies that rejoin to the body, each named by the hash of its bytes", async () => {
const body = Uint8Array.from({ length: 10 }, (_, i) => i);
const { manifest, chunks } = await packContent("x", body, { chunkSizeBytes: 4 });
body.fill(9);
assert.deepEqual(chunks.map(c => c.payload.length), [4, 4, 2]);
assert.deepEqual(Array.from(chunks.flatMap(c => Array.from(c.payload))), [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
for (const [i, c] of chunks.entries()) {
assert.equal(c.chunkHash, await sha256Hex(c.payload));
assert.deepEqual(manifest.chunks[i], { chunkHash: c.chunkHash, offset: i * 4, sizeBytes: c.payload.length, recordKey: chunkRecordKey(c.chunkHash) });
}
const twins = await packContent("x", new Uint8Array(8), { chunkSizeBytes: 4 });
assert.equal(twins.manifest.chunks[0]!.chunkHash, twins.manifest.chunks[1]!.chunkHash, "equal chunks share a hash");
});
test("pack defaults: kind asset, mime octet-stream, empty version, tags and metadata", async () => {
const bare = (await packContent("x", new Uint8Array(3))).manifest;
assert.deepEqual([bare.kind, bare.mimeType, bare.version, bare.tags, bare.metadata], ["asset", "application/octet-stream", "", [], []]);
const blank = (await packContent("x", new Uint8Array(3), { kind: " ", mimeType: "" })).manifest;
assert.deepEqual([blank.kind, blank.mimeType], ["asset", "application/octet-stream"]);
const given = (await packContent("x", new Uint8Array(3), { kind: "build", mimeType: "text/plain", version: "2", tags: ["a"], metadata: [["k", "v"]] })).manifest;
assert.deepEqual([given.kind, given.mimeType, given.version, given.tags, given.metadata], ["build", "text/plain", "2", ["a"], [["k", "v"]]]);
assert.equal(given.artifactId, "x");
});
test("packContent refuses a blank artifact id, a chunk size outside 1..2147483647 and duplicate metadata keys", async () => {
for (const id of ["", " ", "\u0085\u00a0"]) await assert.rejects(packContent(id, new Uint8Array(1)), /Artifact id must be non-empty/);
for (const chunkSizeBytes of [0, -1, 2 ** 31, 1.5, NaN]) await assert.rejects(packContent("x", new Uint8Array(1), { chunkSizeBytes }), /ChunkSizeBytes/);
await packContent("x", new Uint8Array(1), { chunkSizeBytes: 2 ** 31 - 1 });
await assert.rejects(packContent("x", new Uint8Array(1), { metadata: [["a", "1"], ["a", "2"]] }), /duplicate key/);
});
test("default createdAtUtc matches the C# round-trip 'O' format", async () => {
const { manifest } = await packContent("x", new Uint8Array(1));
assert.match(manifest.createdAtUtc, /^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d\.\d{7}\+00:00$/);
assert.ok(Math.abs(Date.parse(manifest.createdAtUtc.replace(/(\.\d{3})\d{4}\+00:00$/, "$1Z")) - Date.now()) < 60_000);
});
test("manifest_out_of_order decodes and validates, and its chunks keep wire order", () => {
const decoded = decodeManifest(csManifest("manifest_out_of_order"));
assert.deepEqual(decoded.chunks.map(c => c.offset / 262_144), [3, 0, 5, 1, 4, 2]);
});
test("metadata is ordered entries, immune to JS integer-key ordering", () => {
assert.deepEqual(decodeManifest(csManifest("manifest_metadata_order")).metadata, [["zeta", "1"], ["alpha", "2"], ["mid", "3"]]);
const metadata = [["10", "a"], ["alpha", "b"], ["2", "c"]] as const;
const bytes = encodeManifest(manifestOf({ metadata }));
assert.deepEqual(decodeManifest(bytes).metadata, metadata);
assert.deepEqual(encodeManifest(decodeManifest(bytes)), bytes);
});
test("a map larger than a fixmap round-trips in entry order", () => {
for (const count of [16, 70_000]) {
const metadata = Array.from({ length: count }, (_, i) => [`k${count - i}`, "v"] as const);
const bytes = encodeManifest(manifestOf({ metadata }));
assert.deepEqual(decodeManifest(bytes).metadata, metadata);
}
});
test("a manifest with a duplicated metadata key is refused, in decode and in validate", () => {
const metadata = [["a", "1"], ["b", "x"], ["a", "2"]] as const;
assert.throws(() => decodeManifest(encodeManifest(manifestOf({ metadata }))), /duplicate key/);
assert.throws(() => validateManifest(manifestOf({ metadata })), /duplicate key/);
validateManifest(manifestOf({ metadata: [["a", "1"], ["b", "1"]] }));
});
test("a metadata key that is not a string, and a __proto__ key, are refused", () => {
const withMap = (...entry: Uint8Array[]) => Uint8Array.from([...wire().slice(0, -5), 0x81, ...entry.flatMap(part => Array.from(part))]);
decodeManifest(withMap(encode("k"), encode("v")));
refusal(withMap(encode(5), encode("v")));
refusal(withMap(encode(new Uint8Array(2)), encode("v")));
refusal(withMap(encode("__proto__"), encode("v")));
});
test("an integer field accepts any width and refuses what a JS number cannot hold exactly", () => {
const empty = { 4: 0, 7: [] as unknown[] };
// Past the integer rule, a size nothing sums to fails the sum check; a refused integer fails the type check first.
assert.throws(() => decodeManifest(wire({ ...empty, 4: 2 ** 53 - 1 })), /do not sum/);
assert.throws(() => decodeManifest(wire({ ...empty, 4: 2n ** 53n - 1n })), /do not sum/);
assert.throws(() => decodeManifest(wire({ ...empty, 4: 2n ** 53n + 1n })), /wrong type/);
assert.throws(() => decodeManifest(wire({ ...empty, 4: -(2n ** 53n) })), /wrong type/);
assert.throws(() => decodeManifest(wire({ ...empty, 4: 0.5 })), /wrong type/);
// A 64-bit encoding of a small value is the same value.
const marked = wire({ 4: 0xdeadbeef, 7: [[HASH, 0, 4, "k"]] });
const at = marked.findIndex((_, i) => marked[i] === 0xce && marked[i + 1] === 0xde && marked[i + 4] === 0xef);
const wide = Uint8Array.from([...marked.slice(0, at), 0xcf, 0, 0, 0, 0, 0, 0, 0, 4, ...marked.slice(at + 5)]);
assert.equal(decodeManifest(wide).sizeBytes, 4);
});
test("a chunk size outside int32 is refused as the reference's int cannot hold it", () => {
const chunk = (size: number) => decodeManifest(wire({ 4: size, 7: [[HASH, 0, size, "k"]] }));
assert.equal(chunk(2 ** 31 - 1).chunks[0]!.sizeBytes, 2 ** 31 - 1);
assert.throws(() => chunk(2 ** 31), /wrong type/);
assert.throws(() => decodeManifest(wire({ 4: -(2 ** 31) - 1, 7: [[HASH, 0, -(2 ** 31) - 1, "k"]] })), /wrong type/);
});
test("a payload nested 257 deep is refused before decoding and 256 is not refused for depth", () => {
const nest = (depth: number) => Uint8Array.from([...Array<number>(depth).fill(0x91), 0xc0]);
assert.throws(() => decodeManifest(nest(257)), CultCachePayloadDepthError);
assert.throws(() => decodeManifest(nest(256)), (error: unknown) => error instanceof Error && !(error instanceof CultCachePayloadDepthError));
// The nesting is inside a field of an otherwise valid manifest: the outer array is depth 1.
const inside = (depth: number) => wire({ 8: Array.from({ length: depth }).reduce<unknown>(value => [value], null) });
assert.throws(() => decodeManifest(inside(256)), CultCachePayloadDepthError);
assert.throws(() => decodeManifest(inside(255)), (error: unknown) => error instanceof Error && !(error instanceof CultCachePayloadDepthError));
});
test("a manifest of the wrong shape is refused naming the field and never the value", () => {
const canary = "CANARY-7f3a91";
for (const bytes of [
wire({ 0: 5 }), wire({ 1: 5 }), wire({ 2: 5 }), wire({ 3: 5 }), wire({ 5: 5 }), wire({ 6: 5 }),
wire({ 4: canary }), wire({ 7: canary }), wire({ 7: [canary] }), wire({ 7: [[HASH, 0, 4]] }), wire({ 7: [[HASH, canary, 4, "k"]] }),
wire({ 7: [[5, 0, 4, "k"]] }), wire({ 7: [[HASH, 0, 4, 5]] }), wire({ 8: canary }), wire({ 8: [5] }), wire({ 9: canary }), wire({ 9: { k: 5 } }),
wire({ 9: null }), wire({ 9: [] }), wire({ 0: null }), encode([canary]), encode(canary), encode(canary).slice(0, 4),
Uint8Array.from([...wire(), 0xc0]), encode([...Array<unknown>(11).fill(canary)]), wire({ 7: [[HASH, 0, 4, "k", canary]] }),
]) {
assert.throws(() => decodeManifest(bytes), (error: unknown) => error instanceof Error && !error.message.includes(canary) && error.message.length > 0);
}
});
test("validateManifest refuses negative size, a gap, an overlap and a size-sum mismatch with the C# messages", () => {
const chunk = (offset: number, sizeBytes: number) => ({ chunkHash: HASH, offset, sizeBytes, recordKey: "k" });
const refuses = (over: Partial<CultMeshCdnArtifactManifest>, message: string) =>
assert.throws(() => validateManifest(manifestOf(over)), new Error(message));
refuses({ sizeBytes: -1, chunks: [] }, "CDN artifact manifest has a negative size.");
refuses({ sizeBytes: 4, chunks: [chunk(0, 2), chunk(3, 1)] }, "CDN artifact chunks are not contiguous.");
refuses({ sizeBytes: 4, chunks: [chunk(0, 3), chunk(2, 2)] }, "CDN artifact chunks are not contiguous.");
refuses({ sizeBytes: 4, chunks: [chunk(1, 4)] }, "CDN artifact chunks are not contiguous.");
refuses({ sizeBytes: 0, chunks: [chunk(0, -1)] }, "CDN artifact chunk has a negative size.");
refuses({ sizeBytes: 5, chunks: [chunk(0, 4)] }, "CDN artifact chunk sizes do not sum to the manifest size.");
refuses({ sizeBytes: 3, chunks: [chunk(0, 4)] }, "CDN artifact chunk sizes do not sum to the manifest size.");
refuses({ sizeBytes: -1, chunks: [chunk(1, 1)] }, "CDN artifact manifest has a negative size.");
validateManifest(manifestOf({ sizeBytes: 4, chunks: [chunk(2, 2), chunk(0, 2)] }));
validateManifest(manifestOf({ sizeBytes: 0, chunks: [] }));
});
test("normalizeHash: 'SHA256:ABC ' -> 'abc', ' sha256:abc' -> 'sha256:abc', blank throws", () => {
assert.equal(normalizeHash("SHA256:ABC "), "abc");
assert.equal(normalizeHash("sHa256:AbC"), "abc");
assert.equal(normalizeHash(" sha256:abc"), "sha256:abc");
assert.equal(normalizeHash("sha256:sha256:ab"), "sha256:ab");
assert.equal(normalizeHash("sha256: "), "");
assert.equal(normalizeHash("\u00a0\u0130X\u00a0"), "\u0130x");
for (const blank of ["", " ", "\u0085\u2003"]) assert.throws(() => normalizeHash(blank, "chunkHash"), /Hash must be non-empty\. \(Parameter 'chunkHash'\)/);
});
test("parseSha256Hex accepts 64 hex digits in any case and prefix, and refuses anything else without echoing it", () => {
assert.equal(parseSha256Hex(`SHA256: ${"AB".repeat(32)} `), "ab".repeat(32));
for (const bad of ["a".repeat(63), "a".repeat(65), `${"a".repeat(63)}g`, "", "sha256:", `sha256:CANARY-${"a".repeat(50)}`]) {
assert.throws(() => parseSha256Hex(bad), (error: unknown) => error instanceof Error && !error.message.includes("CANARY"));
}
});
test("every entry in cdn-record-key-vectors.json derives to its recordKey", async () => {
const file = JSON.parse(readFileSync(join(CONTRACTS, "cdn-record-key-vectors.json"), "utf8")) as {
artifacts: { artifactIdUnits: number[]; versionUnits: number[]; contentHash: string; recordKey: string }[];
chunks: { chunkHash: string; recordKey: string }[];
};
assert.ok(file.artifacts.length >= 14 && file.chunks.length >= 7, "the file carries the C# traps");
const text = (units: number[]) => String.fromCharCode(...units);
for (const a of file.artifacts) {
assert.equal(await artifactRecordKey(text(a.artifactIdUnits), text(a.versionUnits), a.contentHash), a.recordKey, JSON.stringify(a.artifactIdUnits));
}
for (const c of file.chunks) assert.equal(chunkRecordKey(c.chunkHash), c.recordKey, c.chunkHash);
assert.ok(file.artifacts.some(a => a.artifactIdUnits.includes(0xd800)), "a lone surrogate survives the file");
});
test("record keys refuse a blank id, version-less artifacts are 'unversioned', and a blank hash throws", async () => {
await assert.rejects(artifactRecordKey(" ", "1", HASH), /Value must be non-empty\. \(Parameter 'artifactId'\)/);
await assert.rejects(artifactRecordKey("a", "1", " "), /Hash must be non-empty\. \(Parameter 'contentHash'\)/);
assert.throws(() => chunkRecordKey(""), /\(Parameter 'chunkHash'\)/);
assert.equal(await artifactRecordKey("a", " ", HASH), `mesh:cdn:artifact:a:unversioned:${HASH}`);
assert.equal(chunkRecordKey(`SHA256:${HASH.toUpperCase()}`), `mesh:cdn:chunk:${HASH}`);
});
test("sha256Hex of '' and of the fixture body equal the published digests", async () => {
assert.equal(await sha256Hex(new Uint8Array(0)), "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
assert.equal(await sha256Hex(FIXTURE_BODY), FIXTURE_HASH);
assert.equal(await sha256Hex(new TextEncoder().encode("abc")), "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad");
assert.equal(EMPTY_SHA256, await sha256Hex(new Uint8Array(0)));
});
test("esbuild bundles dist/content.js with platform 'browser' without error", async () => {
const result = await build({
entryPoints: [join(__dirname, "..", "..", "dist", "content.js")], bundle: true, platform: "browser", format: "esm", write: false, logLevel: "silent",
});
assert.ok(result.outputFiles[0]!.text.includes("gamecult.mesh.cdn_artifact_manifest.v1"));
});
test("the committed ts-written file re-packs from its declared inputs to the same bytes and record keys", async () => {
type Input = { artifactId: string; kind: string; version: string; mimeType: string; createdAtUtc: string; chunkSizeBytes: number; tags: string[];
metadata: [string, string][]; body: { length: number; multiplier: number; modulus: number } };
const file = JSON.parse(readFileSync(join(CONTRACTS, "content-vectors.ts-written.json"), "utf8")) as
{ vectors: { label: string; kind: string; input: Input; recordKey: string; messagePackBase64: string }[] };
assert.ok(file.vectors.length >= 3);
assert.ok(file.vectors.some(v => v.input.body.length === 0) && file.vectors.some(v => v.input.body.length > v.input.chunkSizeBytes));
for (const { label, kind, input, recordKey, messagePackBase64 } of file.vectors) {
assert.equal(kind, "manifest");
const { body, ...options } = input;
const { manifest } = await packContent(input.artifactId, Uint8Array.from({ length: body.length }, (_, i) => (i * body.multiplier) % body.modulus), options);
const bytes = new Uint8Array(Buffer.from(messagePackBase64, "base64"));
assert.deepEqual(encodeManifest(manifest), bytes, label);
assert.deepEqual(encodeManifest(decodeManifest(bytes)), bytes, label);
assert.equal(await artifactRecordKey(manifest.artifactId, manifest.version, manifest.contentHash), recordKey, label);
}
});

View file

@ -0,0 +1,60 @@
// The TypeScript -> C# direction of the CultMesh content parity check.
//
// `contracts/cultmesh/content-vectors.cs-written.json` and `.rs-written.json` are written by the reference and by Rust
// and decoded by TypeScript. This script writes the mirror file, `contracts/cultmesh/content-vectors.ts-written.json`:
// `packages/cultnet-ts/src/content.ts` packs each declared input and encodes the manifest, and the C# test
// `CultMeshContentVectorTests.TypeScriptManifestVectorsMatchTheReference` judges the committed file with the reference
// only (it decodes the bytes, packs the same input itself and compares, and derives the record key).
//
// Deliberately not the C# file's inputs: a multi-byte artifact id, no version, integer-like metadata keys that a JS object
// would reorder, a different body and chunk size. Agreeing on a copy of the other side's inputs would prove less.
//
// npm run build --workspace packages/cultnet-ts
// node scripts/write-cultmesh-content-vectors.mjs
//
// Nothing here is randomised: rerunning on an unchanged codec rewrites the same bytes.
import { existsSync, writeFileSync } from "node:fs";
import { createRequire } from "node:module";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const modulePath = join(repoRoot, "packages", "cultnet-ts", "dist", "content.js");
const outputPath = join(repoRoot, "contracts", "cultmesh", "content-vectors.ts-written.json");
if (!existsSync(modulePath)) {
throw new Error(`${modulePath} is missing; run 'npm run build --workspace packages/cultnet-ts' first.`);
}
const { packContent, encodeManifest, artifactRecordKey } = createRequire(import.meta.url)(modulePath);
const common = {
artifactId: "Pixel Heroes/H\u00fct \u2603",
kind: "asset",
version: "",
mimeType: "application/x-cultnet-ts-vector",
createdAtUtc: "2026-10-19T00:00:00.0000000+00:00",
chunkSizeBytes: 131072,
tags: ["a", "b"],
metadata: [["10", "x"], ["alpha", "y"], ["2", "z"]],
};
const inputs = [
["ts multi-chunk body", { ...common, body: { length: 777777, multiplier: 7, modulus: 256 } }],
["ts empty body", { ...common, body: { length: 0, multiplier: 7, modulus: 256 } }],
["ts single chunk, versioned", { ...common, version: "V1 Beta", chunkSizeBytes: 4194304, tags: [], metadata: [], body: { length: 1000, multiplier: 7, modulus: 256 } }],
];
const vectors = [];
for (const [label, input] of inputs) {
const { body, ...options } = input;
const bytes = Uint8Array.from({ length: body.length }, (_, index) => (index * body.multiplier) % body.modulus);
const { manifest } = await packContent(input.artifactId, bytes, options);
vectors.push({
label,
kind: "manifest",
input,
recordKey: await artifactRecordKey(manifest.artifactId, manifest.version, manifest.contentHash),
messagePackBase64: Buffer.from(encodeManifest(manifest)).toString("base64"),
});
}
writeFileSync(outputPath, `${JSON.stringify({ vectors }, null, 2)}\n`);
console.log(`wrote ${vectors.length} vectors to ${outputPath}`);