From b25d86a8737432aecee30e6e312dfed49d47eb79 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sun, 11 Oct 2026 20:37:29 +0200 Subject: [PATCH] Docs: DocumentPayload stops direct reads and assignment, not runtime narrowing or a cast The cultcache-ts README, the DocumentPayload comment (comment only) and the cultmesh-browser README say plainly what the opaque type guarantees and what it does not; documentPayloadBytes is the escape for bytes. All commits build. Co-Authored-By: Claude Sonnet 5.5 --- packages/cultcache-ts/README.md | 8 ++++++++ packages/cultcache-ts/src/document.ts | 5 +++++ packages/cultmesh-browser/README.md | 4 +++- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/cultcache-ts/README.md b/packages/cultcache-ts/README.md index d37fa047..c887c1c1 100644 --- a/packages/cultcache-ts/README.md +++ b/packages/cultcache-ts/README.md @@ -39,6 +39,14 @@ realtime frame. At run time it is the same `Uint8Array`, so nothing on the wire time it is opaque: it is assignable to and from neither `Uint8Array` nor `ArrayLike`, so no MessagePack decoder accepts it. +What the type guarantees is that a payload cannot be read or assigned directly: a decoder call, an +assignment to `Uint8Array` and `new Uint8Array(payload)` fail to compile. It does not stop what the +runtime value allows. Because the value is a `Uint8Array`, a check such as `payload instanceof +Uint8Array` narrows it back to bytes, and a cast does the same; a read made that way sees the raw +bytes, which for a codec or formatter document are not the document. Read through +`decodeDocumentPayload`, and when you need the bytes themselves (hashing, signing, a store write) +take them with `documentPayloadBytes`. + - `decodeDocumentPayload(definition, payload)` reads a payload through the definition's formatter, codec or plain map and then its schema. The definition is required. - `encodeDocumentPayload(definition, value)` is the writer: the schema first, then the formatter, diff --git a/packages/cultcache-ts/src/document.ts b/packages/cultcache-ts/src/document.ts index 183bd110..52a5e494 100644 --- a/packages/cultcache-ts/src/document.ts +++ b/packages/cultcache-ts/src/document.ts @@ -102,6 +102,11 @@ const plainDocumentFormatter: CultCacheDocumentFormatter = { * `documentPayloadBytes` where they leave (a store write, a frame encode, hashing or signing). * * The key is a plain string, not a unique symbol, so two installed copies of this package stay compatible. + * + * What the type guarantees: a payload cannot be read or assigned directly (a decoder call, `const b: Uint8Array = p`, + * `new Uint8Array(p)` fail to compile). What it does not: the runtime value is a `Uint8Array`, so runtime narrowing + * (`p instanceof Uint8Array`) or a cast gets the raw bytes back, and a codec or formatter document read that way is not + * the document. The escape for bytes is `documentPayloadBytes`; the reader is `decodeDocumentPayload`. */ export interface DocumentPayload { readonly "cultcache.documentPayload": true; diff --git a/packages/cultmesh-browser/README.md b/packages/cultmesh-browser/README.md index 58aef6d2..e130171c 100644 --- a/packages/cultmesh-browser/README.md +++ b/packages/cultmesh-browser/README.md @@ -34,7 +34,9 @@ A record's `payload` is a `DocumentPayload`, an opaque type over the same bytes, `decodeCultNetPayload(record, definition)` is how it is read: through the CultCache document definition that says whether the bytes are a plain map, a codec's slot record or a formatter's, then through the definition's schema. A plain MessagePack decode of a payload -does not compile, and `decodeCultNetPayload` takes no definition-less form. The definition +does not compile, and `decodeCultNetPayload` takes no definition-less form. The type stops direct reads +and assignment; it does not stop runtime narrowing (`payload instanceof Uint8Array`) or a cast, which +would read the raw bytes. For the bytes themselves use `documentPayloadBytes`. The definition comes from `@gamecult/cultcache-ts` (this package imports only its `./document` entry, which holds no Node built-ins).