@@ -13,6 +13,168 @@ const JSON_COMPAT_BIGINT = "$BigInt";
1313const JSON_COMPAT_ARRAY_BUFFER = "$ArrayBuffer" ;
1414const JSON_COMPAT_UINT8_ARRAY = "$Uint8Array" ;
1515const JSON_COMPAT_UNDEFINED = "$Undefined" ;
16+ const JSON_COMPAT_SET = "$Set" ;
17+
18+ /**
19+ * Recursive type representing all values that can be serialized via CBOR
20+ * (cbor-x). Matches the supported CBOR tag set: primitives, BigInt (tags 2/3),
21+ * Date (tags 0/1), Error (tag 27), TypedArrays (tags 64-82), ArrayBuffer,
22+ * Map (tag 259), Set (custom $Set encoding), arrays, and plain objects.
23+ */
24+ export type CborSerializable =
25+ | string
26+ | number
27+ | boolean
28+ | null
29+ | undefined
30+ | bigint
31+ | Date
32+ | Error
33+ | ArrayBuffer
34+ | Uint8Array
35+ | Uint8ClampedArray
36+ | Uint16Array
37+ | Uint32Array
38+ | BigUint64Array
39+ | Int8Array
40+ | Int16Array
41+ | Int32Array
42+ | BigInt64Array
43+ | Float32Array
44+ | Float64Array
45+ | CborSerializable [ ]
46+ | Map < CborSerializable , CborSerializable >
47+ | Set < CborSerializable >
48+ | { [ key : string ] : CborSerializable } ;
49+
50+ function isTypedArray ( value : unknown ) : boolean {
51+ return (
52+ value instanceof Uint8ClampedArray ||
53+ value instanceof Uint16Array ||
54+ value instanceof Uint32Array ||
55+ value instanceof BigUint64Array ||
56+ value instanceof Int8Array ||
57+ value instanceof Int16Array ||
58+ value instanceof Int32Array ||
59+ value instanceof BigInt64Array ||
60+ value instanceof Float32Array ||
61+ value instanceof Float64Array
62+ ) ;
63+ }
64+
65+ /**
66+ * Recursively validates that a value is CBOR serializable. Throws TypeError
67+ * with a descriptive message for non-serializable values.
68+ */
69+ export function assertCborSerializable (
70+ value : unknown ,
71+ path = "" ,
72+ ) : asserts value is CborSerializable {
73+ if (
74+ value === null ||
75+ value === undefined ||
76+ typeof value === "string" ||
77+ typeof value === "number" ||
78+ typeof value === "boolean" ||
79+ typeof value === "bigint"
80+ ) {
81+ return ;
82+ }
83+
84+ if ( typeof value === "function" ) {
85+ throw new TypeError (
86+ `Value at ${ path || "root" } is a function and is not CBOR serializable` ,
87+ ) ;
88+ }
89+
90+ if ( typeof value === "symbol" ) {
91+ throw new TypeError (
92+ `Value at ${ path || "root" } is a symbol and is not CBOR serializable` ,
93+ ) ;
94+ }
95+
96+ if (
97+ value instanceof Date ||
98+ value instanceof Error ||
99+ value instanceof ArrayBuffer ||
100+ value instanceof Uint8Array ||
101+ isTypedArray ( value )
102+ ) {
103+ return ;
104+ }
105+
106+ if ( value instanceof RegExp ) {
107+ throw new TypeError (
108+ `Value at ${ path || "root" } is a RegExp and is not CBOR serializable` ,
109+ ) ;
110+ }
111+
112+ if ( value instanceof WeakMap ) {
113+ throw new TypeError (
114+ `Value at ${ path || "root" } is a WeakMap and is not CBOR serializable` ,
115+ ) ;
116+ }
117+
118+ if ( value instanceof WeakSet ) {
119+ throw new TypeError (
120+ `Value at ${ path || "root" } is a WeakSet and is not CBOR serializable` ,
121+ ) ;
122+ }
123+
124+ if ( value instanceof WeakRef ) {
125+ throw new TypeError (
126+ `Value at ${ path || "root" } is a WeakRef and is not CBOR serializable` ,
127+ ) ;
128+ }
129+
130+ if ( value instanceof Promise ) {
131+ throw new TypeError (
132+ `Value at ${ path || "root" } is a Promise and is not CBOR serializable` ,
133+ ) ;
134+ }
135+
136+ if ( value instanceof Map ) {
137+ for ( const [ k , v ] of value . entries ( ) ) {
138+ assertCborSerializable ( k , `${ path || "root" } .key(${ String ( k ) } )` ) ;
139+ assertCborSerializable ( v , `${ path || "root" } .value(${ String ( k ) } )` ) ;
140+ }
141+ return ;
142+ }
143+
144+ if ( value instanceof Set ) {
145+ let index = 0 ;
146+ for ( const item of value . values ( ) ) {
147+ assertCborSerializable ( item , `${ path || "root" } .set[${ index } ]` ) ;
148+ index ++ ;
149+ }
150+ return ;
151+ }
152+
153+ if ( Array . isArray ( value ) ) {
154+ for ( let i = 0 ; i < value . length ; i ++ ) {
155+ assertCborSerializable ( value [ i ] , `${ path || "root" } [${ i } ]` ) ;
156+ }
157+ return ;
158+ }
159+
160+ if ( isPlainObject ( value ) ) {
161+ for ( const key in value ) {
162+ assertCborSerializable (
163+ value [ key as keyof typeof value ] ,
164+ path ? `${ path } .${ key } ` : key ,
165+ ) ;
166+ }
167+ return ;
168+ }
169+
170+ const typeName =
171+ typeof value === "object" && value !== null
172+ ? value . constructor ?. name ?? typeof value
173+ : typeof value ;
174+ throw new TypeError (
175+ `Value at ${ path || "root" } of type "${ typeName } " is not CBOR serializable` ,
176+ ) ;
177+ }
16178
17179export const EncodingSchema = z . enum ( [ "json" , "cbor" , "bare" ] ) ;
18180
@@ -112,38 +274,95 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
112274 return proto === Object . prototype || proto === null ;
113275}
114276
115- export function encodeJsonCompatValue ( input : any ) : any {
277+ export function encodeJsonCompatValue ( input : CborSerializable ) : unknown {
278+ // Primitives
279+ if ( input === null ) {
280+ return input ;
281+ }
116282 if ( input === undefined ) {
117283 return [ JSON_COMPAT_UNDEFINED , 0 ] ;
118284 }
285+ if (
286+ typeof input === "string" ||
287+ typeof input === "number" ||
288+ typeof input === "boolean"
289+ ) {
290+ return input ;
291+ }
119292 if ( typeof input === "bigint" ) {
120293 return [ JSON_COMPAT_BIGINT , input . toString ( ) ] ;
121294 }
295+
296+ // Binary types with custom encoding
122297 if ( input instanceof ArrayBuffer ) {
123298 return [ JSON_COMPAT_ARRAY_BUFFER , base64EncodeArrayBuffer ( input ) ] ;
124299 }
125300 if ( input instanceof Uint8Array ) {
126301 return [ JSON_COMPAT_UINT8_ARRAY , base64EncodeUint8Array ( input ) ] ;
127302 }
303+
304+ // TypedArrays pass through for cbor-x native handling
305+ if ( isTypedArray ( input ) ) {
306+ return input ;
307+ }
308+
309+ // Date and Error pass through for cbor-x native handling
310+ if ( input instanceof Date || input instanceof Error ) {
311+ return input ;
312+ }
313+
314+ // Set uses custom tag encoding
315+ if ( input instanceof Set ) {
316+ const encoded = [ ...input . values ( ) ] . map ( ( v ) =>
317+ encodeJsonCompatValue ( v as CborSerializable ) ,
318+ ) ;
319+ return [ JSON_COMPAT_SET , encoded ] ;
320+ }
321+
322+ // Map recurses into keys and values
323+ if ( input instanceof Map ) {
324+ const encoded = new Map < unknown , unknown > ( ) ;
325+ for ( const [ k , v ] of input . entries ( ) ) {
326+ encoded . set (
327+ encodeJsonCompatValue ( k as CborSerializable ) ,
328+ encodeJsonCompatValue ( v as CborSerializable ) ,
329+ ) ;
330+ }
331+ return encoded ;
332+ }
333+
334+ // Arrays
128335 if ( Array . isArray ( input ) ) {
129- const encoded = input . map ( ( value ) => encodeJsonCompatValue ( value ) ) ;
336+ const encoded = input . map ( ( value ) =>
337+ encodeJsonCompatValue ( value as CborSerializable ) ,
338+ ) ;
130339 if (
131340 encoded . length === 2 &&
132341 typeof encoded [ 0 ] === "string" &&
133- encoded [ 0 ] . startsWith ( "$" )
342+ ( encoded [ 0 ] as string ) . startsWith ( "$" )
134343 ) {
135344 return [ "$" + encoded [ 0 ] , encoded [ 1 ] ] ;
136345 }
137346 return encoded ;
138347 }
348+
349+ // Plain objects
139350 if ( isPlainObject ( input ) ) {
140351 const encoded : Record < string , unknown > = { } ;
141352 for ( const [ key , value ] of Object . entries ( input ) ) {
142- encoded [ key ] = encodeJsonCompatValue ( value ) ;
353+ encoded [ key ] = encodeJsonCompatValue ( value as CborSerializable ) ;
143354 }
144355 return encoded ;
145356 }
146- return input ;
357+
358+ // Not serializable
359+ const typeName =
360+ typeof input === "object" && input !== null
361+ ? ( input as object ) . constructor ?. name ?? typeof input
362+ : typeof input ;
363+ throw new TypeError (
364+ `Value of type "${ typeName } " is not CBOR serializable` ,
365+ ) ;
147366}
148367
149368export interface JsonCompatReviveOptions {
@@ -182,6 +401,12 @@ export function reviveJsonCompatValue(
182401 if ( input [ 0 ] === JSON_COMPAT_UNDEFINED ) {
183402 return undefined ;
184403 }
404+ if ( input [ 0 ] === JSON_COMPAT_SET ) {
405+ const items = ( input [ 1 ] as unknown [ ] ) . map ( ( v ) =>
406+ reviveJsonCompatValue ( v , options ) ,
407+ ) ;
408+ return new Set ( items ) ;
409+ }
185410 if ( input [ 0 ] . startsWith ( "$$" ) ) {
186411 return [
187412 input [ 0 ] . substring ( 1 ) ,
0 commit comments