@zudojs/serialization 1.0.1 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -5
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/serializerEnvelope/envelope.core.d.ts +6 -2
- package/dist/serializerEnvelope/envelope.core.js +15 -10
- package/dist/serializerJson/index.d.ts +1 -0
- package/dist/serializerJson/index.js +1 -0
- package/dist/serializerJson/jsonSerializer.core.d.ts +13 -6
- package/dist/serializerJson/jsonSerializer.core.js +36 -131
- package/dist/serializerJson/jsonSerializer.escape.d.ts +35 -0
- package/dist/serializerJson/jsonSerializer.escape.js +50 -0
- package/dist/serializerJson/jsonSerializer.keys.d.ts +21 -0
- package/dist/serializerJson/jsonSerializer.keys.js +41 -0
- package/dist/serializerJson/jsonSerializer.restore.d.ts +17 -0
- package/dist/serializerJson/jsonSerializer.restore.js +86 -0
- package/dist/serializerJson/jsonSerializer.transform.d.ts +17 -0
- package/dist/serializerJson/jsonSerializer.transform.js +59 -0
- package/dist/serializerRegistry/serializerRegistry.factory.d.ts +9 -3
- package/dist/serializerRegistry/serializerRegistry.factory.js +7 -13
- package/dist/serializerTransforms/bigint.transformer.js +22 -8
- package/dist/serializerTransforms/date.transformer.js +6 -2
- package/dist/serializerTransforms/map.transformer.js +2 -1
- package/dist/serializerTransforms/set.transformer.js +2 -1
- package/dist/serializerTransforms/transformerRegistry.core.d.ts +5 -1
- package/dist/serializerTransforms/transformerRegistry.core.js +7 -3
- package/dist/serializerTransformsExt/buffer.transformer.js +2 -1
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
JSON serialization with optional type preservation, a transformer registry, and an envelope for cross-service payloads.
|
|
4
4
|
|
|
5
|
+
<!-- zudo-docs:start -->
|
|
6
|
+
|
|
7
|
+
**Documentation:** [zudojs.oyinlola.site/docs/packages-serialization](https://zudojs.oyinlola.site/docs/packages-serialization) · **For AI agents:** [Markdown version](https://zudojs.oyinlola.site/docs/packages-serialization.md), [llms.txt](https://zudojs.oyinlola.site/llms.txt)
|
|
8
|
+
|
|
9
|
+
<!-- zudo-docs:end -->
|
|
10
|
+
|
|
5
11
|
## Installation
|
|
6
12
|
|
|
7
13
|
```bash
|
|
@@ -22,8 +28,10 @@ const value = serializer.deserialize<{ id: string; createdAt: Date }>(json);
|
|
|
22
28
|
value.createdAt instanceof Date; // true
|
|
23
29
|
```
|
|
24
30
|
|
|
25
|
-
|
|
26
|
-
|
|
31
|
+
Every serialize/deserialize option — `pretty`, `preserveTypes`, `maxSize`,
|
|
32
|
+
`maxDepth`, `strict`, `allowUnsafeKeys`, `includeStack` — can be passed to
|
|
33
|
+
`createSerializer` as an instance default. The methods are `serialize` /
|
|
34
|
+
`deserialize`, and every option can be overridden per call:
|
|
27
35
|
|
|
28
36
|
```typescript
|
|
29
37
|
serializer.serialize(value, { pretty: true, maxSize: 1_000_000 });
|
|
@@ -84,12 +92,32 @@ RPC peer or a request body.
|
|
|
84
92
|
- **Input is size-bounded** by `maxSize`, and depth-bounded by `maxDepth`. On
|
|
85
93
|
the `preserveTypes` path the depth limit defaults to 128; on the fast path it
|
|
86
94
|
is enforced whenever you set it, per call or as an instance default.
|
|
87
|
-
- **
|
|
88
|
-
|
|
89
|
-
|
|
95
|
+
- **Your own data may carry a `$type` key.** With `preserveTypes`, a plain
|
|
96
|
+
object that has its own string `$type` is written escaped as
|
|
97
|
+
`{"$type":"Object","$value":{...}}` (`ESCAPED_OBJECT_TAG`) and read back as
|
|
98
|
+
the plain object it was, so user data can never be revived as a `Map`,
|
|
99
|
+
`Error`, `BigInt` or `Buffer`. `"Object"` is reserved and cannot be
|
|
100
|
+
registered as a transformer. Payloads written before this escaping existed
|
|
101
|
+
are still read as before: an unescaped tag is revived, since it cannot be
|
|
102
|
+
told apart from one the serializer wrote.
|
|
103
|
+
- **An unrecognised or malformed tag is treated as ordinary data**, so a peer
|
|
104
|
+
cannot stop the consumer with `{"$type":"anything"}` or
|
|
105
|
+
`{"$type":"Date","$value":"nope"}`, and a bad record stays readable. Pass
|
|
106
|
+
`strict: true` to make either an error instead.
|
|
107
|
+
- **BigInt tags are capped at 4096 decimal digits**
|
|
108
|
+
(`SerializationLimits.MAX_BIGINT_DIGITS` from `@zudojs/constants`, shared
|
|
109
|
+
with `@zudojs/schema`'s `coerce.bigint()`), checked before `BigInt()` runs;
|
|
110
|
+
serializing a larger BigInt throws.
|
|
90
111
|
|
|
91
112
|
## Errors
|
|
92
113
|
|
|
114
|
+
Failures throw `@zudojs/errors` serialization classes: `SerializationPayloadTooLargeError`,
|
|
115
|
+
`SerializationDepthError`, `InvalidSerializedDataError` (malformed JSON, unknown
|
|
116
|
+
or malformed tags under `strict`), `TransformerError` and `SerializeError` (for
|
|
117
|
+
example an invalid `Date`). All extend `SerializationError`.
|
|
118
|
+
|
|
119
|
+
### Serialized errors
|
|
120
|
+
|
|
93
121
|
Stack traces are **not** serialized unless you ask, because a serialized error
|
|
94
122
|
routinely ends up in a queue message or a log sink:
|
|
95
123
|
|
package/dist/index.d.ts
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* ```
|
|
26
26
|
*/
|
|
27
27
|
export type { SerializationFormat, SerializedValue, SerializeOptions, DeserializeOptions, SerializationMetadata, SerializedEnvelope, Serializer, TypeTransformer, } from "./serializerTypes/index.js";
|
|
28
|
-
export { JSONSerializer } from "./serializerJson/index.js";
|
|
28
|
+
export { JSONSerializer, ESCAPED_OBJECT_TAG } from "./serializerJson/index.js";
|
|
29
29
|
export { TransformerRegistry } from "./serializerTransforms/index.js";
|
|
30
30
|
export { DateTransformer } from "./serializerTransforms/index.js";
|
|
31
31
|
export { BigIntTransformer } from "./serializerTransforms/index.js";
|
package/dist/index.js
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* ```
|
|
26
26
|
*/
|
|
27
27
|
// ─── JSON Serializer ──────────────────────────────────────────
|
|
28
|
-
export { JSONSerializer } from "./serializerJson/index.js";
|
|
28
|
+
export { JSONSerializer, ESCAPED_OBJECT_TAG } from "./serializerJson/index.js";
|
|
29
29
|
// ─── Transformer Registry ─────────────────────────────────────
|
|
30
30
|
export { TransformerRegistry } from "./serializerTransforms/index.js";
|
|
31
31
|
export { DateTransformer } from "./serializerTransforms/index.js";
|
|
@@ -30,7 +30,10 @@ export declare function contentTypeForFormat(format: string): string;
|
|
|
30
30
|
* Validates that a value received from the wire is a well-formed envelope.
|
|
31
31
|
*
|
|
32
32
|
* @param envelope - The candidate envelope.
|
|
33
|
-
* @throws {
|
|
33
|
+
* @throws {InvalidSerializedDataError} when the shape or schema version is
|
|
34
|
+
* unusable. Envelope payloads arrive from a queue or an RPC peer, so a
|
|
35
|
+
* rejection has to be distinguishable from an internal bug: a bare `Error`
|
|
36
|
+
* left callers unable to tell hostile input from a defect of their own.
|
|
34
37
|
*/
|
|
35
38
|
export declare function assertValidEnvelope(envelope: unknown): asserts envelope is SerializedEnvelope;
|
|
36
39
|
/**
|
|
@@ -39,7 +42,8 @@ export declare function assertValidEnvelope(envelope: unknown): asserts envelope
|
|
|
39
42
|
* @param envelope - The envelope to unwrap.
|
|
40
43
|
* @param expectedFormat - Optional format to validate against.
|
|
41
44
|
* @returns The raw serialized data.
|
|
42
|
-
* @throws {
|
|
45
|
+
* @throws {InvalidSerializedDataError} when the envelope is malformed or its
|
|
46
|
+
* format doesn't match expectations.
|
|
43
47
|
*/
|
|
44
48
|
export declare function unwrapEnvelope(envelope: SerializedEnvelope, expectedFormat?: string): SerializedValue;
|
|
45
49
|
/**
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* messaging, queues, RPC, and cross-service communication.
|
|
7
7
|
*/
|
|
8
8
|
import { SerializationFormat, SerializationContentType, SERIALIZATION_SCHEMA_VERSION, } from "@zudojs/constants";
|
|
9
|
+
import { InvalidSerializedDataError } from "@zudojs/errors";
|
|
9
10
|
import { decodeUtf8 } from "../serializerTransformsExt/index.js";
|
|
10
11
|
/**
|
|
11
12
|
* Create a serialization envelope wrapping data with metadata.
|
|
@@ -51,32 +52,35 @@ export function contentTypeForFormat(format) {
|
|
|
51
52
|
* Validates that a value received from the wire is a well-formed envelope.
|
|
52
53
|
*
|
|
53
54
|
* @param envelope - The candidate envelope.
|
|
54
|
-
* @throws {
|
|
55
|
+
* @throws {InvalidSerializedDataError} when the shape or schema version is
|
|
56
|
+
* unusable. Envelope payloads arrive from a queue or an RPC peer, so a
|
|
57
|
+
* rejection has to be distinguishable from an internal bug: a bare `Error`
|
|
58
|
+
* left callers unable to tell hostile input from a defect of their own.
|
|
55
59
|
*/
|
|
56
60
|
export function assertValidEnvelope(envelope) {
|
|
57
61
|
if (typeof envelope !== "object" || envelope === null) {
|
|
58
|
-
throw new
|
|
62
|
+
throw new InvalidSerializedDataError(`Malformed envelope: expected an object, got ${envelope === null ? "null" : typeof envelope}`, { format: "envelope" });
|
|
59
63
|
}
|
|
60
64
|
const candidate = envelope;
|
|
61
65
|
if (typeof candidate.metadata !== "object" || candidate.metadata === null) {
|
|
62
|
-
throw new
|
|
66
|
+
throw new InvalidSerializedDataError("Malformed envelope: missing metadata", { format: "envelope" });
|
|
63
67
|
}
|
|
64
68
|
const metadata = candidate.metadata;
|
|
65
69
|
if (typeof metadata.format !== "string" || metadata.format.length === 0) {
|
|
66
|
-
throw new
|
|
70
|
+
throw new InvalidSerializedDataError("Malformed envelope: metadata.format is missing", { format: "envelope" });
|
|
67
71
|
}
|
|
68
72
|
if (typeof candidate.data !== "string" &&
|
|
69
73
|
!(candidate.data instanceof Uint8Array)) {
|
|
70
|
-
throw new
|
|
74
|
+
throw new InvalidSerializedDataError("Malformed envelope: data must be a string or Uint8Array", { format: "envelope" });
|
|
71
75
|
}
|
|
72
76
|
// The version exists so a future producer can be detected rather than
|
|
73
77
|
// silently misread. Older versions stay readable; newer ones do not.
|
|
74
78
|
if (metadata.version !== undefined) {
|
|
75
79
|
if (!Number.isInteger(metadata.version)) {
|
|
76
|
-
throw new
|
|
80
|
+
throw new InvalidSerializedDataError(`Malformed envelope: metadata.version must be an integer, got ${String(metadata.version)}`, { format: "envelope" });
|
|
77
81
|
}
|
|
78
82
|
if (metadata.version > SERIALIZATION_SCHEMA_VERSION) {
|
|
79
|
-
throw new
|
|
83
|
+
throw new InvalidSerializedDataError(`Unsupported envelope schema version ${metadata.version}: this build understands up to ${SERIALIZATION_SCHEMA_VERSION}`, { format: "envelope" });
|
|
80
84
|
}
|
|
81
85
|
}
|
|
82
86
|
}
|
|
@@ -86,12 +90,13 @@ export function assertValidEnvelope(envelope) {
|
|
|
86
90
|
* @param envelope - The envelope to unwrap.
|
|
87
91
|
* @param expectedFormat - Optional format to validate against.
|
|
88
92
|
* @returns The raw serialized data.
|
|
89
|
-
* @throws {
|
|
93
|
+
* @throws {InvalidSerializedDataError} when the envelope is malformed or its
|
|
94
|
+
* format doesn't match expectations.
|
|
90
95
|
*/
|
|
91
96
|
export function unwrapEnvelope(envelope, expectedFormat) {
|
|
92
97
|
assertValidEnvelope(envelope);
|
|
93
98
|
if (expectedFormat && envelope.metadata.format !== expectedFormat) {
|
|
94
|
-
throw new
|
|
99
|
+
throw new InvalidSerializedDataError(`Envelope format mismatch: expected "${expectedFormat}", got "${envelope.metadata.format}"`, { format: expectedFormat });
|
|
95
100
|
}
|
|
96
101
|
return envelope.data;
|
|
97
102
|
}
|
|
@@ -132,7 +137,7 @@ export function deserializeFromEnvelope(envelope, deserializer, expectedFormat,
|
|
|
132
137
|
}
|
|
133
138
|
const encoding = (envelope.metadata.encoding ?? "utf-8").toLowerCase();
|
|
134
139
|
if (encoding !== "utf-8" && encoding !== "utf8") {
|
|
135
|
-
throw new
|
|
140
|
+
throw new InvalidSerializedDataError(`Unsupported envelope encoding "${envelope.metadata.encoding}": only UTF-8 is supported`, { format: envelope.metadata.format });
|
|
136
141
|
}
|
|
137
142
|
return deserializer.deserialize(decodeUtf8(data), options);
|
|
138
143
|
}
|
|
@@ -7,6 +7,12 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import type { Serializer, SerializeOptions, DeserializeOptions } from "../serializerTypes/index.js";
|
|
9
9
|
import { TransformerRegistry } from "../serializerTransforms/index.js";
|
|
10
|
+
/**
|
|
11
|
+
* JSON serializer. With `preserveTypes`, plain objects that carry their own
|
|
12
|
+
* string `$type` key are escaped as `{"$type":"Object","$value":{...}}` on
|
|
13
|
+
* write and unwrapped on read, so user data can never be revived as a
|
|
14
|
+
* different runtime type.
|
|
15
|
+
*/
|
|
10
16
|
export declare class JSONSerializer implements Serializer<unknown, string> {
|
|
11
17
|
readonly name = "json";
|
|
12
18
|
readonly contentType = "application/json";
|
|
@@ -17,14 +23,15 @@ export declare class JSONSerializer implements Serializer<unknown, string> {
|
|
|
17
23
|
/** Default options merged into every call. */
|
|
18
24
|
readonly defaults?: SerializeOptions & DeserializeOptions;
|
|
19
25
|
});
|
|
20
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* Register a custom type transformer.
|
|
28
|
+
*
|
|
29
|
+
* @throws {TransformerError} for the reserved escape tag `"Object"`.
|
|
30
|
+
*/
|
|
21
31
|
registerTransformer(transformer: import("../serializerTypes/index.js").TypeTransformer): void;
|
|
22
32
|
serialize(value: unknown, options?: SerializeOptions): string;
|
|
23
33
|
deserialize<T = unknown>(value: string, options?: DeserializeOptions): T;
|
|
24
|
-
|
|
25
|
-
private
|
|
26
|
-
private assertValidJson;
|
|
27
|
-
private assertOutputSize;
|
|
28
|
-
private assertInputSize;
|
|
34
|
+
/** Parses JSON, reporting malformed input as a typed error. */
|
|
35
|
+
private parse;
|
|
29
36
|
}
|
|
30
37
|
//# sourceMappingURL=jsonSerializer.core.d.ts.map
|
|
@@ -5,12 +5,15 @@
|
|
|
5
5
|
* Advanced path (preserveTypes: true): recursive traversal with
|
|
6
6
|
* transformer-based type preservation.
|
|
7
7
|
*/
|
|
8
|
-
import { TransformerRegistry } from "../serializerTransforms/index.js";
|
|
9
|
-
import { DateTransformer, BigIntTransformer, MapTransformer, SetTransformer, } from "../serializerTransforms/index.js";
|
|
8
|
+
import { TransformerRegistry, DateTransformer, BigIntTransformer, MapTransformer, SetTransformer, } from "../serializerTransforms/index.js";
|
|
10
9
|
import { BufferTransformer, ErrorTransformer, } from "../serializerTransformsExt/index.js";
|
|
11
|
-
import { SerializationLimits
|
|
12
|
-
import {
|
|
10
|
+
import { SerializationLimits } from "@zudojs/constants";
|
|
11
|
+
import { InvalidSerializedDataError, TransformerError } from "@zudojs/errors";
|
|
13
12
|
import { assertNoCircularReference, assertDepthWithinLimit, } from "@zudojs/validation";
|
|
13
|
+
import { ESCAPED_OBJECT_TAG } from "./jsonSerializer.escape.js";
|
|
14
|
+
import { assertByteSize } from "./jsonSerializer.keys.js";
|
|
15
|
+
import { transformValue } from "./jsonSerializer.transform.js";
|
|
16
|
+
import { restoreValue } from "./jsonSerializer.restore.js";
|
|
14
17
|
/** Default transformer registry with all built-in transformers. */
|
|
15
18
|
function createDefaultTransformers() {
|
|
16
19
|
const registry = new TransformerRegistry();
|
|
@@ -22,6 +25,12 @@ function createDefaultTransformers() {
|
|
|
22
25
|
registry.register(ErrorTransformer);
|
|
23
26
|
return registry;
|
|
24
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* JSON serializer. With `preserveTypes`, plain objects that carry their own
|
|
30
|
+
* string `$type` key are escaped as `{"$type":"Object","$value":{...}}` on
|
|
31
|
+
* write and unwrapped on read, so user data can never be revived as a
|
|
32
|
+
* different runtime type.
|
|
33
|
+
*/
|
|
25
34
|
export class JSONSerializer {
|
|
26
35
|
name = "json";
|
|
27
36
|
contentType = "application/json";
|
|
@@ -31,8 +40,15 @@ export class JSONSerializer {
|
|
|
31
40
|
this.transformers = options?.transformers ?? createDefaultTransformers();
|
|
32
41
|
this.defaults = options?.defaults ?? {};
|
|
33
42
|
}
|
|
34
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* Register a custom type transformer.
|
|
45
|
+
*
|
|
46
|
+
* @throws {TransformerError} for the reserved escape tag `"Object"`.
|
|
47
|
+
*/
|
|
35
48
|
registerTransformer(transformer) {
|
|
49
|
+
if (transformer.type === ESCAPED_OBJECT_TAG) {
|
|
50
|
+
throw new TransformerError(transformer.type, `"${ESCAPED_OBJECT_TAG}" is reserved for escaped plain objects.`);
|
|
51
|
+
}
|
|
36
52
|
this.transformers.register(transformer);
|
|
37
53
|
}
|
|
38
54
|
serialize(value, options) {
|
|
@@ -40,13 +56,17 @@ export class JSONSerializer {
|
|
|
40
56
|
const maxDepth = opts.maxDepth ?? SerializationLimits.MAX_DEPTH;
|
|
41
57
|
const maxSize = opts.maxSize ?? SerializationLimits.MAX_SIZE;
|
|
42
58
|
if (opts.preserveTypes === true) {
|
|
43
|
-
|
|
59
|
+
// The cycle check runs first and must therefore honour the caller's
|
|
60
|
+
// depth limit: with its own 512-level ceiling it halted on a deep but
|
|
61
|
+
// perfectly acyclic payload and reported a cycle that did not exist,
|
|
62
|
+
// disagreeing with the fast path below for the same input.
|
|
63
|
+
assertNoCircularReference(value, "root", maxDepth);
|
|
44
64
|
assertDepthWithinLimit(value, maxDepth);
|
|
45
|
-
const transformed =
|
|
65
|
+
const transformed = transformValue({ transformers: this.transformers, maxDepth, options: opts }, value, 0);
|
|
46
66
|
const json = opts.pretty
|
|
47
67
|
? JSON.stringify(transformed, null, opts.indent ?? 2)
|
|
48
68
|
: JSON.stringify(transformed);
|
|
49
|
-
|
|
69
|
+
assertByteSize(json, maxSize);
|
|
50
70
|
return json;
|
|
51
71
|
}
|
|
52
72
|
// The fast path stays a bare `JSON.stringify` by default, but a depth
|
|
@@ -59,7 +79,7 @@ export class JSONSerializer {
|
|
|
59
79
|
const json = opts.pretty
|
|
60
80
|
? JSON.stringify(value, null, opts.indent ?? 2)
|
|
61
81
|
: JSON.stringify(value);
|
|
62
|
-
|
|
82
|
+
assertByteSize(json, maxSize);
|
|
63
83
|
return json;
|
|
64
84
|
}
|
|
65
85
|
deserialize(value, options) {
|
|
@@ -69,14 +89,12 @@ export class JSONSerializer {
|
|
|
69
89
|
// the string handed to `deserialize` arrives from a queue, an RPC peer, or
|
|
70
90
|
// a request body.
|
|
71
91
|
const maxSize = opts.maxSize ?? SerializationLimits.MAX_SIZE;
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
this.assertValidJson(value);
|
|
75
|
-
const parsed = JSON.parse(value);
|
|
92
|
+
assertByteSize(value, maxSize);
|
|
93
|
+
const parsed = this.parse(value);
|
|
76
94
|
if (opts.preserveTypes === true) {
|
|
77
95
|
const maxDepth = opts.maxDepth ?? SerializationLimits.MAX_DEPTH;
|
|
78
96
|
assertDepthWithinLimit(parsed, maxDepth);
|
|
79
|
-
return
|
|
97
|
+
return restoreValue({ transformers: this.transformers, maxDepth, options: opts }, parsed, 0);
|
|
80
98
|
}
|
|
81
99
|
// Same contract on the fast path: an explicit `maxDepth` bounds input
|
|
82
100
|
// that arrives from the wire, whether or not types are being restored.
|
|
@@ -85,127 +103,14 @@ export class JSONSerializer {
|
|
|
85
103
|
}
|
|
86
104
|
return parsed;
|
|
87
105
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
return value;
|
|
91
|
-
if (typeof value === "bigint") {
|
|
92
|
-
const transformer = this.transformers.findForValue(value);
|
|
93
|
-
return transformer
|
|
94
|
-
? transformer.serialize(value, options)
|
|
95
|
-
: value.toString();
|
|
96
|
-
}
|
|
97
|
-
if (typeof value !== "object")
|
|
98
|
-
return value;
|
|
99
|
-
if (depth >= maxDepth) {
|
|
100
|
-
// Depth is pre-checked by assertDepthWithinLimit, so this is a belt-and
|
|
101
|
-
// braces guard. Returning the raw value would silently emit an untagged
|
|
102
|
-
// Map or Date, which cannot round-trip — fail loudly instead.
|
|
103
|
-
throw new Error(`Serialization exceeded maximum depth of ${maxDepth}`);
|
|
104
|
-
}
|
|
105
|
-
if (Array.isArray(value)) {
|
|
106
|
-
return value.map((item) => this.transformValue(item, depth + 1, maxDepth, options));
|
|
107
|
-
}
|
|
108
|
-
const transformer = this.transformers.findForValue(value);
|
|
109
|
-
if (transformer) {
|
|
110
|
-
const raw = transformer.serialize(value, options);
|
|
111
|
-
return this.transformValue(raw, depth + 1, maxDepth, options);
|
|
112
|
-
}
|
|
113
|
-
if (isPlainObject(value)) {
|
|
114
|
-
const result = {};
|
|
115
|
-
for (const key of Object.keys(value)) {
|
|
116
|
-
defineKey(result, key, this.transformValue(value[key], depth + 1, maxDepth, options), options.allowUnsafeKeys === true);
|
|
117
|
-
}
|
|
118
|
-
return result;
|
|
119
|
-
}
|
|
120
|
-
return value;
|
|
121
|
-
}
|
|
122
|
-
restoreValue(value, depth, maxDepth, options) {
|
|
123
|
-
if (value === null || value === undefined)
|
|
124
|
-
return value;
|
|
125
|
-
if (typeof value !== "object")
|
|
126
|
-
return value;
|
|
127
|
-
if (depth >= maxDepth) {
|
|
128
|
-
throw new Error(`Deserialization exceeded maximum depth of ${maxDepth}`);
|
|
129
|
-
}
|
|
130
|
-
if (Array.isArray(value)) {
|
|
131
|
-
return value.map((item) => this.restoreValue(item, depth + 1, maxDepth, options));
|
|
132
|
-
}
|
|
133
|
-
const obj = value;
|
|
134
|
-
const typeTag = obj[SerializationTags.TYPE];
|
|
135
|
-
if (typeof typeTag === "string" && this.transformers.has(typeTag)) {
|
|
136
|
-
// Restore the children first. A transformer receives a plain structure
|
|
137
|
-
// and has no way to recurse back into this serializer, so handing it the
|
|
138
|
-
// still-tagged payload is what used to leave a Map full of raw
|
|
139
|
-
// `{$type, $value}` objects.
|
|
140
|
-
const restoredShell = {};
|
|
141
|
-
for (const key of Object.keys(obj)) {
|
|
142
|
-
defineKey(restoredShell, key, key === SerializationTags.TYPE
|
|
143
|
-
? obj[key]
|
|
144
|
-
: this.restoreValue(obj[key], depth + 1, maxDepth, options), options.allowUnsafeKeys === true);
|
|
145
|
-
}
|
|
146
|
-
return this.transformers.get(typeTag).deserialize(restoredShell, options);
|
|
147
|
-
}
|
|
148
|
-
// An unknown tag is ordinary data. Throwing here let any peer crash the
|
|
149
|
-
// consumer with `{"$type":"anything"}`, and made legitimate payloads that
|
|
150
|
-
// happen to carry a `$type` field unparseable.
|
|
151
|
-
if (typeof typeTag === "string" && options.strict === true) {
|
|
152
|
-
throw new Error(`Unknown serialization type tag: "${typeTag}". ` +
|
|
153
|
-
"Register a transformer for it, or deserialize without strict mode.");
|
|
154
|
-
}
|
|
155
|
-
if (isPlainObject(value)) {
|
|
156
|
-
const result = {};
|
|
157
|
-
for (const key of Object.keys(value)) {
|
|
158
|
-
defineKey(result, key, this.restoreValue(value[key], depth + 1, maxDepth, options), options.allowUnsafeKeys === true);
|
|
159
|
-
}
|
|
160
|
-
return result;
|
|
161
|
-
}
|
|
162
|
-
return value;
|
|
163
|
-
}
|
|
164
|
-
assertValidJson(value) {
|
|
106
|
+
/** Parses JSON, reporting malformed input as a typed error. */
|
|
107
|
+
parse(value) {
|
|
165
108
|
try {
|
|
166
|
-
JSON.parse(value);
|
|
109
|
+
return JSON.parse(value);
|
|
167
110
|
}
|
|
168
111
|
catch (err) {
|
|
169
|
-
throw new
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
assertOutputSize(json, maxSize) {
|
|
173
|
-
const size = byteLength(json);
|
|
174
|
-
if (size > maxSize) {
|
|
175
|
-
throw new Error(`Serialized payload too large: ${size} bytes (max: ${maxSize})`);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
assertInputSize(json, maxSize) {
|
|
179
|
-
const size = byteLength(json);
|
|
180
|
-
if (size > maxSize) {
|
|
181
|
-
throw new Error(`Serialized payload too large: ${size} bytes (max: ${maxSize})`);
|
|
112
|
+
throw new InvalidSerializedDataError(`Invalid JSON: ${err.message}`, { format: "json", cause: err });
|
|
182
113
|
}
|
|
183
114
|
}
|
|
184
115
|
}
|
|
185
|
-
/** Byte length of a string, in whichever runtime we are on. */
|
|
186
|
-
function byteLength(value) {
|
|
187
|
-
return typeof Buffer !== "undefined"
|
|
188
|
-
? Buffer.byteLength(value, "utf-8")
|
|
189
|
-
: new TextEncoder().encode(value).byteLength;
|
|
190
|
-
}
|
|
191
|
-
/**
|
|
192
|
-
* Assigns a key onto a freshly built object without invoking a setter.
|
|
193
|
-
*
|
|
194
|
-
* Plain assignment of `__proto__` does not create an own property — it calls
|
|
195
|
-
* the inherited setter and replaces the object's prototype, so an attacker's
|
|
196
|
-
* keys resolve on the result while `Object.keys` shows nothing. `defineProperty`
|
|
197
|
-
* always creates a real own property, and forbidden keys are dropped outright
|
|
198
|
-
* unless the caller has explicitly opted in with `allowUnsafeKeys`.
|
|
199
|
-
*/
|
|
200
|
-
function defineKey(target, key, value, allowUnsafeKeys) {
|
|
201
|
-
if (!allowUnsafeKeys && SCHEMA_FORBIDDEN_KEYS.has(key)) {
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
Object.defineProperty(target, key, {
|
|
205
|
-
value,
|
|
206
|
-
writable: true,
|
|
207
|
-
enumerable: true,
|
|
208
|
-
configurable: true,
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
116
|
//# sourceMappingURL=jsonSerializer.core.js.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/serialization — Escaping of user objects that look like type tags.
|
|
3
|
+
*
|
|
4
|
+
* With `preserveTypes`, a tagged object `{ "$type": "Map", "$value": [...] }`
|
|
5
|
+
* is revived into a real `Map` on read. A *user* object carrying its own
|
|
6
|
+
* string `$type` key used to be written out verbatim, so any caller who
|
|
7
|
+
* controlled part of a payload could choose the runtime type the reader got
|
|
8
|
+
* back (an `Error`, a `Buffer`, a `BigInt`), or plant a malformed tag that
|
|
9
|
+
* made the record unreadable.
|
|
10
|
+
*
|
|
11
|
+
* Such objects are now written wrapped in a reserved tag:
|
|
12
|
+
*
|
|
13
|
+
* ```json
|
|
14
|
+
* { "$type": "Object", "$value": { "$type": "Map", "note": "user data" } }
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* and unwrapped on read into the plain object they were, with their own
|
|
18
|
+
* `$type` key left uninterpreted.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Reserved type tag that marks an escaped plain object. No transformer may
|
|
22
|
+
* register under this name.
|
|
23
|
+
*/
|
|
24
|
+
export declare const ESCAPED_OBJECT_TAG = "Object";
|
|
25
|
+
/** True when a plain object would be mistaken for a type tag on read. */
|
|
26
|
+
export declare function needsEscape(value: Record<string, unknown>): boolean;
|
|
27
|
+
/** Wraps an already-transformed object body in the reserved escape tag. */
|
|
28
|
+
export declare function escapeObject(body: Record<string, unknown>): Record<string, unknown>;
|
|
29
|
+
/**
|
|
30
|
+
* Returns the escaped body when `value` is a well-formed escape wrapper,
|
|
31
|
+
* `undefined` when it is not an escape wrapper at all, and `null` when it
|
|
32
|
+
* claims to be one but its `$value` is not a plain object.
|
|
33
|
+
*/
|
|
34
|
+
export declare function escapedBody(value: Record<string, unknown>): Record<string, unknown> | null | undefined;
|
|
35
|
+
//# sourceMappingURL=jsonSerializer.escape.d.ts.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/serialization — Escaping of user objects that look like type tags.
|
|
3
|
+
*
|
|
4
|
+
* With `preserveTypes`, a tagged object `{ "$type": "Map", "$value": [...] }`
|
|
5
|
+
* is revived into a real `Map` on read. A *user* object carrying its own
|
|
6
|
+
* string `$type` key used to be written out verbatim, so any caller who
|
|
7
|
+
* controlled part of a payload could choose the runtime type the reader got
|
|
8
|
+
* back (an `Error`, a `Buffer`, a `BigInt`), or plant a malformed tag that
|
|
9
|
+
* made the record unreadable.
|
|
10
|
+
*
|
|
11
|
+
* Such objects are now written wrapped in a reserved tag:
|
|
12
|
+
*
|
|
13
|
+
* ```json
|
|
14
|
+
* { "$type": "Object", "$value": { "$type": "Map", "note": "user data" } }
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* and unwrapped on read into the plain object they were, with their own
|
|
18
|
+
* `$type` key left uninterpreted.
|
|
19
|
+
*/
|
|
20
|
+
import { SerializationTags } from "@zudojs/constants";
|
|
21
|
+
import { isPlainObject } from "@zudojs/types";
|
|
22
|
+
/**
|
|
23
|
+
* Reserved type tag that marks an escaped plain object. No transformer may
|
|
24
|
+
* register under this name.
|
|
25
|
+
*/
|
|
26
|
+
export const ESCAPED_OBJECT_TAG = "Object";
|
|
27
|
+
/** True when a plain object would be mistaken for a type tag on read. */
|
|
28
|
+
export function needsEscape(value) {
|
|
29
|
+
return (Object.hasOwn(value, SerializationTags.TYPE) &&
|
|
30
|
+
typeof value[SerializationTags.TYPE] === "string");
|
|
31
|
+
}
|
|
32
|
+
/** Wraps an already-transformed object body in the reserved escape tag. */
|
|
33
|
+
export function escapeObject(body) {
|
|
34
|
+
return {
|
|
35
|
+
[SerializationTags.TYPE]: ESCAPED_OBJECT_TAG,
|
|
36
|
+
[SerializationTags.VALUE]: body,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Returns the escaped body when `value` is a well-formed escape wrapper,
|
|
41
|
+
* `undefined` when it is not an escape wrapper at all, and `null` when it
|
|
42
|
+
* claims to be one but its `$value` is not a plain object.
|
|
43
|
+
*/
|
|
44
|
+
export function escapedBody(value) {
|
|
45
|
+
if (value[SerializationTags.TYPE] !== ESCAPED_OBJECT_TAG)
|
|
46
|
+
return undefined;
|
|
47
|
+
const body = value[SerializationTags.VALUE];
|
|
48
|
+
return isPlainObject(body) ? body : null;
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=jsonSerializer.escape.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/serialization — Low-level helpers shared by the JSON walkers.
|
|
3
|
+
*/
|
|
4
|
+
/** Byte length of a string, in whichever runtime we are on. */
|
|
5
|
+
export declare function byteLength(value: string): number;
|
|
6
|
+
/**
|
|
7
|
+
* Throws `SerializationPayloadTooLargeError` when a JSON string is larger
|
|
8
|
+
* than `maxSize` bytes.
|
|
9
|
+
*/
|
|
10
|
+
export declare function assertByteSize(json: string, maxSize: number): void;
|
|
11
|
+
/**
|
|
12
|
+
* Assigns a key onto a freshly built object without invoking a setter.
|
|
13
|
+
*
|
|
14
|
+
* Plain assignment of `__proto__` does not create an own property — it calls
|
|
15
|
+
* the inherited setter and replaces the object's prototype, so an attacker's
|
|
16
|
+
* keys resolve on the result while `Object.keys` shows nothing. `defineProperty`
|
|
17
|
+
* always creates a real own property, and forbidden keys are dropped outright
|
|
18
|
+
* unless the caller has explicitly opted in with `allowUnsafeKeys`.
|
|
19
|
+
*/
|
|
20
|
+
export declare function defineKey(target: Record<string, unknown>, key: string, value: unknown, allowUnsafeKeys: boolean): void;
|
|
21
|
+
//# sourceMappingURL=jsonSerializer.keys.d.ts.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/serialization — Low-level helpers shared by the JSON walkers.
|
|
3
|
+
*/
|
|
4
|
+
import { SCHEMA_FORBIDDEN_KEYS } from "@zudojs/constants";
|
|
5
|
+
import { SerializationPayloadTooLargeError } from "@zudojs/errors";
|
|
6
|
+
/** Byte length of a string, in whichever runtime we are on. */
|
|
7
|
+
export function byteLength(value) {
|
|
8
|
+
return typeof Buffer !== "undefined"
|
|
9
|
+
? Buffer.byteLength(value, "utf-8")
|
|
10
|
+
: new TextEncoder().encode(value).byteLength;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Throws `SerializationPayloadTooLargeError` when a JSON string is larger
|
|
14
|
+
* than `maxSize` bytes.
|
|
15
|
+
*/
|
|
16
|
+
export function assertByteSize(json, maxSize) {
|
|
17
|
+
const size = byteLength(json);
|
|
18
|
+
if (size > maxSize)
|
|
19
|
+
throw new SerializationPayloadTooLargeError(size, maxSize);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Assigns a key onto a freshly built object without invoking a setter.
|
|
23
|
+
*
|
|
24
|
+
* Plain assignment of `__proto__` does not create an own property — it calls
|
|
25
|
+
* the inherited setter and replaces the object's prototype, so an attacker's
|
|
26
|
+
* keys resolve on the result while `Object.keys` shows nothing. `defineProperty`
|
|
27
|
+
* always creates a real own property, and forbidden keys are dropped outright
|
|
28
|
+
* unless the caller has explicitly opted in with `allowUnsafeKeys`.
|
|
29
|
+
*/
|
|
30
|
+
export function defineKey(target, key, value, allowUnsafeKeys) {
|
|
31
|
+
if (!allowUnsafeKeys && SCHEMA_FORBIDDEN_KEYS.has(key)) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
Object.defineProperty(target, key, {
|
|
35
|
+
value,
|
|
36
|
+
writable: true,
|
|
37
|
+
enumerable: true,
|
|
38
|
+
configurable: true,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=jsonSerializer.keys.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/serialization — Deserialize-side walker (`preserveTypes: true`).
|
|
3
|
+
*
|
|
4
|
+
* Revives tagged values through their transformers and unwraps escaped
|
|
5
|
+
* plain objects (see `jsonSerializer.escape.ts`).
|
|
6
|
+
*/
|
|
7
|
+
import type { DeserializeOptions } from "../serializerTypes/index.js";
|
|
8
|
+
import type { TransformerRegistry } from "../serializerTransforms/index.js";
|
|
9
|
+
/** What the walker needs besides the value. */
|
|
10
|
+
export interface RestoreWalk {
|
|
11
|
+
readonly transformers: TransformerRegistry;
|
|
12
|
+
readonly maxDepth: number;
|
|
13
|
+
readonly options: DeserializeOptions;
|
|
14
|
+
}
|
|
15
|
+
/** Rebuilds runtime values from their tagged JSON representation. */
|
|
16
|
+
export declare function restoreValue(walk: RestoreWalk, value: unknown, depth: number): unknown;
|
|
17
|
+
//# sourceMappingURL=jsonSerializer.restore.d.ts.map
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/serialization — Deserialize-side walker (`preserveTypes: true`).
|
|
3
|
+
*
|
|
4
|
+
* Revives tagged values through their transformers and unwraps escaped
|
|
5
|
+
* plain objects (see `jsonSerializer.escape.ts`).
|
|
6
|
+
*/
|
|
7
|
+
import { InvalidSerializedDataError, SerializationDepthError, TransformerError, isSerializationError, } from "@zudojs/errors";
|
|
8
|
+
import { SerializationLimits, SerializationTags } from "@zudojs/constants";
|
|
9
|
+
import { isPlainObject } from "@zudojs/types";
|
|
10
|
+
import { escapedBody } from "./jsonSerializer.escape.js";
|
|
11
|
+
import { defineKey } from "./jsonSerializer.keys.js";
|
|
12
|
+
/** Longest prefix of a rejected tag quoted back in an error message. */
|
|
13
|
+
const TAG_EXCERPT_LENGTH = 64;
|
|
14
|
+
/** Clips a wire-supplied tag so it cannot flood an error message or a log line. */
|
|
15
|
+
function describeTag(tag) {
|
|
16
|
+
return tag.length <= TAG_EXCERPT_LENGTH
|
|
17
|
+
? tag
|
|
18
|
+
: `${tag.slice(0, TAG_EXCERPT_LENGTH)}…`;
|
|
19
|
+
}
|
|
20
|
+
/** Rebuilds runtime values from their tagged JSON representation. */
|
|
21
|
+
export function restoreValue(walk, value, depth) {
|
|
22
|
+
if (value === null || typeof value !== "object")
|
|
23
|
+
return value;
|
|
24
|
+
if (depth >= walk.maxDepth) {
|
|
25
|
+
throw new SerializationDepthError(depth + 1, walk.maxDepth);
|
|
26
|
+
}
|
|
27
|
+
if (Array.isArray(value)) {
|
|
28
|
+
return value.map((item) => restoreValue(walk, item, depth + 1));
|
|
29
|
+
}
|
|
30
|
+
const obj = value;
|
|
31
|
+
const strict = walk.options.strict === true;
|
|
32
|
+
const body = escapedBody(obj);
|
|
33
|
+
if (body)
|
|
34
|
+
return restoreEntries(walk, body, depth);
|
|
35
|
+
if (body === null && strict) {
|
|
36
|
+
throw new InvalidSerializedDataError('Escaped object tag "Object" must carry a plain-object $value.', { format: "json" });
|
|
37
|
+
}
|
|
38
|
+
const tag = obj[SerializationTags.TYPE];
|
|
39
|
+
if (body === undefined && typeof tag === "string") {
|
|
40
|
+
// The tag arrives from the wire, so bound it before it is looked up or
|
|
41
|
+
// quoted into a message: `SerializationLimits.MAX_TYPE_TAG_LENGTH` was
|
|
42
|
+
// exported and tested but never enforced, which let a 100 000-character
|
|
43
|
+
// tag be interpolated verbatim into an error and from there into a log.
|
|
44
|
+
if (tag.length > SerializationLimits.MAX_TYPE_TAG_LENGTH) {
|
|
45
|
+
throw new InvalidSerializedDataError(`Serialization type tag exceeds ${SerializationLimits.MAX_TYPE_TAG_LENGTH} characters ` +
|
|
46
|
+
`(got ${tag.length}): "${describeTag(tag)}".`, { format: "json" });
|
|
47
|
+
}
|
|
48
|
+
if (walk.transformers.has(tag))
|
|
49
|
+
return revive(walk, tag, obj, depth);
|
|
50
|
+
// An unknown tag is ordinary data unless the caller asked for strictness.
|
|
51
|
+
if (strict) {
|
|
52
|
+
throw new InvalidSerializedDataError(`Unknown serialization type tag: "${describeTag(tag)}". ` +
|
|
53
|
+
"Register a transformer for it, or deserialize without strict mode.", { format: "json" });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return isPlainObject(obj) ? restoreEntries(walk, obj, depth) : obj;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Hands a tag to its transformer after restoring the tag's children.
|
|
60
|
+
*
|
|
61
|
+
* A tag the transformer rejects (say an unparseable Date) used to make the
|
|
62
|
+
* whole record permanently unreadable. Outside strict mode it now reads back
|
|
63
|
+
* as the plain object it is; strict mode throws a typed `TransformerError`.
|
|
64
|
+
*/
|
|
65
|
+
function revive(walk, tag, obj, depth) {
|
|
66
|
+
const shell = restoreEntries(walk, obj, depth, SerializationTags.TYPE);
|
|
67
|
+
try {
|
|
68
|
+
return walk.transformers.get(tag).deserialize(shell, walk.options);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (walk.options.strict !== true)
|
|
72
|
+
return shell;
|
|
73
|
+
if (isSerializationError(error))
|
|
74
|
+
throw error;
|
|
75
|
+
throw new TransformerError(tag, error.message, { cause: error });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** Restores each own key of a plain object into a fresh object. */
|
|
79
|
+
function restoreEntries(walk, obj, depth, verbatimKey) {
|
|
80
|
+
const result = {};
|
|
81
|
+
for (const key of Object.keys(obj)) {
|
|
82
|
+
defineKey(result, key, key === verbatimKey ? obj[key] : restoreValue(walk, obj[key], depth + 1), walk.options.allowUnsafeKeys === true);
|
|
83
|
+
}
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=jsonSerializer.restore.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/serialization — Serialize-side walker (`preserveTypes: true`).
|
|
3
|
+
*
|
|
4
|
+
* Replaces values that have a registered transformer with their tagged form,
|
|
5
|
+
* and escapes plain objects that would otherwise read back as a tag.
|
|
6
|
+
*/
|
|
7
|
+
import type { SerializeOptions } from "../serializerTypes/index.js";
|
|
8
|
+
import type { TransformerRegistry } from "../serializerTransforms/index.js";
|
|
9
|
+
/** What the walker needs besides the value. */
|
|
10
|
+
export interface TransformWalk {
|
|
11
|
+
readonly transformers: TransformerRegistry;
|
|
12
|
+
readonly maxDepth: number;
|
|
13
|
+
readonly options: SerializeOptions;
|
|
14
|
+
}
|
|
15
|
+
/** Converts a value into its JSON-safe, tagged representation. */
|
|
16
|
+
export declare function transformValue(walk: TransformWalk, value: unknown, depth: number): unknown;
|
|
17
|
+
//# sourceMappingURL=jsonSerializer.transform.d.ts.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/serialization — Serialize-side walker (`preserveTypes: true`).
|
|
3
|
+
*
|
|
4
|
+
* Replaces values that have a registered transformer with their tagged form,
|
|
5
|
+
* and escapes plain objects that would otherwise read back as a tag.
|
|
6
|
+
*/
|
|
7
|
+
import { SerializationDepthError } from "@zudojs/errors";
|
|
8
|
+
import { isPlainObject } from "@zudojs/types";
|
|
9
|
+
import { escapeObject, needsEscape } from "./jsonSerializer.escape.js";
|
|
10
|
+
import { defineKey } from "./jsonSerializer.keys.js";
|
|
11
|
+
/** Converts a value into its JSON-safe, tagged representation. */
|
|
12
|
+
export function transformValue(walk, value, depth) {
|
|
13
|
+
if (value === null || value === undefined)
|
|
14
|
+
return value;
|
|
15
|
+
if (typeof value === "bigint") {
|
|
16
|
+
const transformer = walk.transformers.findForValue(value);
|
|
17
|
+
return transformer
|
|
18
|
+
? transformer.serialize(value, walk.options)
|
|
19
|
+
: value.toString();
|
|
20
|
+
}
|
|
21
|
+
if (typeof value !== "object")
|
|
22
|
+
return value;
|
|
23
|
+
if (depth >= walk.maxDepth) {
|
|
24
|
+
// Depth is pre-checked by assertDepthWithinLimit, so this is a belt-and
|
|
25
|
+
// braces guard. Returning the raw value would silently emit an untagged
|
|
26
|
+
// Map or Date, which cannot round-trip — fail loudly instead.
|
|
27
|
+
throw new SerializationDepthError(depth + 1, walk.maxDepth);
|
|
28
|
+
}
|
|
29
|
+
if (Array.isArray(value)) {
|
|
30
|
+
const out = [];
|
|
31
|
+
for (let i = 0; i < value.length; i++) {
|
|
32
|
+
out.push(transformValue(walk, value[i], depth + 1));
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
const transformer = walk.transformers.findForValue(value);
|
|
37
|
+
if (transformer) {
|
|
38
|
+
const raw = transformer.serialize(value, walk.options);
|
|
39
|
+
// The transformer's own tag object is not user data: its children are
|
|
40
|
+
// walked, but it is never escaped.
|
|
41
|
+
return isPlainObject(raw)
|
|
42
|
+
? transformEntries(walk, raw, depth + 1)
|
|
43
|
+
: transformValue(walk, raw, depth + 1);
|
|
44
|
+
}
|
|
45
|
+
if (isPlainObject(value)) {
|
|
46
|
+
const body = transformEntries(walk, value, depth);
|
|
47
|
+
return needsEscape(value) ? escapeObject(body) : body;
|
|
48
|
+
}
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
/** Transforms each own enumerable key of a plain object. */
|
|
52
|
+
function transformEntries(walk, value, depth) {
|
|
53
|
+
const result = {};
|
|
54
|
+
for (const key of Object.keys(value)) {
|
|
55
|
+
defineKey(result, key, transformValue(walk, value[key], depth + 1), walk.options.allowUnsafeKeys === true);
|
|
56
|
+
}
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=jsonSerializer.transform.js.map
|
|
@@ -5,12 +5,18 @@
|
|
|
5
5
|
* Provides a clean public API for creating serializers without
|
|
6
6
|
* exposing concrete class constructors.
|
|
7
7
|
*/
|
|
8
|
-
import type { Serializer, SerializationFormat } from "../serializerTypes/index.js";
|
|
8
|
+
import type { Serializer, SerializationFormat, SerializeOptions, DeserializeOptions } from "../serializerTypes/index.js";
|
|
9
9
|
import { SerializationFormat as Format } from "@zudojs/constants";
|
|
10
10
|
import { TransformerRegistry } from "../serializerTransforms/index.js";
|
|
11
11
|
import { SerializerRegistry } from "./serializerRegistry.core.js";
|
|
12
|
-
/**
|
|
13
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Options accepted when creating a serializer.
|
|
14
|
+
*
|
|
15
|
+
* Every serialize/deserialize option (`pretty`, `preserveTypes`, `maxDepth`,
|
|
16
|
+
* `maxSize`, `strict`, `allowUnsafeKeys`, ...) is accepted as a per-instance
|
|
17
|
+
* default that each call can still override.
|
|
18
|
+
*/
|
|
19
|
+
export interface CreateSerializerOptions extends SerializeOptions, DeserializeOptions {
|
|
14
20
|
/** Transformer registry to use instead of the built-in one. */
|
|
15
21
|
readonly transformers?: TransformerRegistry;
|
|
16
22
|
/** Pretty-print by default. Overridable per call. */
|
|
@@ -12,19 +12,13 @@ import { TransformerRegistry } from "../serializerTransforms/index.js";
|
|
|
12
12
|
import { SerializerRegistry } from "./serializerRegistry.core.js";
|
|
13
13
|
export function createSerializer(format, options) {
|
|
14
14
|
switch (format) {
|
|
15
|
-
case Format.JSON:
|
|
16
|
-
// `pretty` and `preserveTypes` used to be
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
...(options?.pretty !== undefined ? { pretty: options.pretty } : {}),
|
|
23
|
-
...(options?.preserveTypes !== undefined
|
|
24
|
-
? { preserveTypes: options.preserveTypes }
|
|
25
|
-
: {}),
|
|
26
|
-
},
|
|
27
|
-
});
|
|
15
|
+
case Format.JSON: {
|
|
16
|
+
// Only `pretty` and `preserveTypes` used to be forwarded, so instance
|
|
17
|
+
// limits such as `maxDepth`/`maxSize`/`strict` were silently dropped.
|
|
18
|
+
const { transformers, ...rest } = options ?? {};
|
|
19
|
+
const defaults = Object.fromEntries(Object.entries(rest).filter(([, value]) => value !== undefined));
|
|
20
|
+
return new JSONSerializer({ transformers, defaults });
|
|
21
|
+
}
|
|
28
22
|
default:
|
|
29
23
|
throw new UnsupportedSerializationFormatError(format);
|
|
30
24
|
}
|
|
@@ -4,8 +4,18 @@
|
|
|
4
4
|
* Preserves BigInt values across serialization boundaries
|
|
5
5
|
* using string representation.
|
|
6
6
|
*/
|
|
7
|
-
import { SerializationTags } from "@zudojs/constants";
|
|
7
|
+
import { SerializationLimits, SerializationTags } from "@zudojs/constants";
|
|
8
|
+
import { InvalidSerializedDataError, SerializeError } from "@zudojs/errors";
|
|
8
9
|
const BIGINT_TYPE = "BigInt";
|
|
10
|
+
/**
|
|
11
|
+
* Most decimal digits a BigInt may carry across the wire. `BigInt(raw)` is
|
|
12
|
+
* super-linear in the digit count, so one 10 MB `{"$type":"BigInt"}` payload
|
|
13
|
+
* used to stall a consumer for seconds. Shared with `@zudojs/schema`'s
|
|
14
|
+
* coercion cap through `SerializationLimits.MAX_BIGINT_DIGITS`.
|
|
15
|
+
*/
|
|
16
|
+
const MAX_BIGINT_DIGITS = SerializationLimits.MAX_BIGINT_DIGITS;
|
|
17
|
+
/** Plain decimal integer, bounded, as `bigint.toString()` writes it. */
|
|
18
|
+
const BIGINT_PATTERN = new RegExp(`^-?\\d{1,${MAX_BIGINT_DIGITS}}$`);
|
|
9
19
|
/** Transformer that handles BigInt round-trips. */
|
|
10
20
|
export const BigIntTransformer = {
|
|
11
21
|
type: BIGINT_TYPE,
|
|
@@ -13,23 +23,27 @@ export const BigIntTransformer = {
|
|
|
13
23
|
return typeof value === "bigint";
|
|
14
24
|
},
|
|
15
25
|
serialize(value) {
|
|
26
|
+
const text = value.toString();
|
|
27
|
+
if (!BIGINT_PATTERN.test(text)) {
|
|
28
|
+
throw new SerializeError(`BigInt exceeds ${MAX_BIGINT_DIGITS} digits and could not be read back.`);
|
|
29
|
+
}
|
|
16
30
|
return {
|
|
17
31
|
[SerializationTags.TYPE]: BIGINT_TYPE,
|
|
18
|
-
[SerializationTags.VALUE]:
|
|
32
|
+
[SerializationTags.VALUE]: text,
|
|
19
33
|
};
|
|
20
34
|
},
|
|
21
35
|
deserialize(value) {
|
|
22
36
|
const data = value;
|
|
23
37
|
const raw = data[SerializationTags.VALUE];
|
|
24
38
|
if (typeof raw !== "string") {
|
|
25
|
-
throw new
|
|
26
|
-
}
|
|
27
|
-
try {
|
|
28
|
-
return BigInt(raw);
|
|
39
|
+
throw new InvalidSerializedDataError(`Invalid BigInt serialized value: expected string, got ${typeof raw}`);
|
|
29
40
|
}
|
|
30
|
-
|
|
31
|
-
|
|
41
|
+
// Checked before `BigInt()` runs, so an oversized value costs a regex
|
|
42
|
+
// scan bounded by the digit limit rather than a multi-second parse.
|
|
43
|
+
if (!BIGINT_PATTERN.test(raw)) {
|
|
44
|
+
throw new InvalidSerializedDataError(`Invalid BigInt serialized value: expected at most ${MAX_BIGINT_DIGITS} decimal digits`);
|
|
32
45
|
}
|
|
46
|
+
return BigInt(raw);
|
|
33
47
|
},
|
|
34
48
|
};
|
|
35
49
|
//# sourceMappingURL=bigint.transformer.js.map
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* using ISO-8601 string representation.
|
|
6
6
|
*/
|
|
7
7
|
import { SerializationTags } from "@zudojs/constants";
|
|
8
|
+
import { InvalidSerializedDataError, SerializeError } from "@zudojs/errors";
|
|
8
9
|
const DATE_TYPE = "Date";
|
|
9
10
|
/** Transformer that handles Date round-trips. */
|
|
10
11
|
export const DateTransformer = {
|
|
@@ -13,6 +14,9 @@ export const DateTransformer = {
|
|
|
13
14
|
return value instanceof Date;
|
|
14
15
|
},
|
|
15
16
|
serialize(value) {
|
|
17
|
+
if (Number.isNaN(value.getTime())) {
|
|
18
|
+
throw new SerializeError("Cannot serialize an invalid Date.");
|
|
19
|
+
}
|
|
16
20
|
return {
|
|
17
21
|
[SerializationTags.TYPE]: DATE_TYPE,
|
|
18
22
|
[SerializationTags.VALUE]: value.toISOString(),
|
|
@@ -22,11 +26,11 @@ export const DateTransformer = {
|
|
|
22
26
|
const data = value;
|
|
23
27
|
const raw = data[SerializationTags.VALUE];
|
|
24
28
|
if (typeof raw !== "string") {
|
|
25
|
-
throw new
|
|
29
|
+
throw new InvalidSerializedDataError(`Invalid Date serialized value: expected string, got ${typeof raw}`);
|
|
26
30
|
}
|
|
27
31
|
const date = new Date(raw);
|
|
28
32
|
if (Number.isNaN(date.getTime())) {
|
|
29
|
-
throw new
|
|
33
|
+
throw new InvalidSerializedDataError(`Invalid Date value: "${raw}"`);
|
|
30
34
|
}
|
|
31
35
|
return date;
|
|
32
36
|
},
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* using an array-of-entries representation.
|
|
6
6
|
*/
|
|
7
7
|
import { SerializationTags } from "@zudojs/constants";
|
|
8
|
+
import { InvalidSerializedDataError } from "@zudojs/errors";
|
|
8
9
|
const MAP_TYPE = "Map";
|
|
9
10
|
/** Transformer that handles Map round-trips. */
|
|
10
11
|
export const MapTransformer = {
|
|
@@ -26,7 +27,7 @@ export const MapTransformer = {
|
|
|
26
27
|
const data = value;
|
|
27
28
|
const raw = data[SerializationTags.VALUE];
|
|
28
29
|
if (!Array.isArray(raw)) {
|
|
29
|
-
throw new
|
|
30
|
+
throw new InvalidSerializedDataError(`Invalid Map serialized value: expected array, got ${typeof raw}`);
|
|
30
31
|
}
|
|
31
32
|
return new Map(raw);
|
|
32
33
|
},
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* using an array representation.
|
|
6
6
|
*/
|
|
7
7
|
import { SerializationTags } from "@zudojs/constants";
|
|
8
|
+
import { InvalidSerializedDataError } from "@zudojs/errors";
|
|
8
9
|
const SET_TYPE = "Set";
|
|
9
10
|
/** Transformer that handles Set round-trips. */
|
|
10
11
|
export const SetTransformer = {
|
|
@@ -22,7 +23,7 @@ export const SetTransformer = {
|
|
|
22
23
|
const data = value;
|
|
23
24
|
const raw = data[SerializationTags.VALUE];
|
|
24
25
|
if (!Array.isArray(raw)) {
|
|
25
|
-
throw new
|
|
26
|
+
throw new InvalidSerializedDataError(`Invalid Set serialized value: expected array, got ${typeof raw}`);
|
|
26
27
|
}
|
|
27
28
|
return new Set(raw);
|
|
28
29
|
},
|
|
@@ -13,7 +13,11 @@ import type { TypeTransformer } from "../serializerTypes/index.js";
|
|
|
13
13
|
*/
|
|
14
14
|
export declare class TransformerRegistry {
|
|
15
15
|
private readonly transformers;
|
|
16
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* Register a transformer.
|
|
18
|
+
*
|
|
19
|
+
* @throws {TransformerError} when the registry is already full.
|
|
20
|
+
*/
|
|
17
21
|
register(transformer: TypeTransformer): void;
|
|
18
22
|
/** Unregister a transformer by type tag. */
|
|
19
23
|
unregister(type: string): boolean;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Manages type transformers that handle custom JS types during
|
|
5
5
|
* serialization and deserialization. Keyed by type tag string.
|
|
6
6
|
*/
|
|
7
|
-
import { TransformerNotFoundError } from "@zudojs/errors";
|
|
7
|
+
import { TransformerError, TransformerNotFoundError } from "@zudojs/errors";
|
|
8
8
|
import { SerializationLimits } from "@zudojs/constants";
|
|
9
9
|
/**
|
|
10
10
|
* Registry of type transformers keyed by their type tag.
|
|
@@ -14,10 +14,14 @@ import { SerializationLimits } from "@zudojs/constants";
|
|
|
14
14
|
*/
|
|
15
15
|
export class TransformerRegistry {
|
|
16
16
|
transformers = new Map();
|
|
17
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* Register a transformer.
|
|
19
|
+
*
|
|
20
|
+
* @throws {TransformerError} when the registry is already full.
|
|
21
|
+
*/
|
|
18
22
|
register(transformer) {
|
|
19
23
|
if (this.transformers.size >= SerializationLimits.MAX_TRANSFORMERS) {
|
|
20
|
-
throw new
|
|
24
|
+
throw new TransformerError(transformer.type, `Maximum transformer limit (${SerializationLimits.MAX_TRANSFORMERS}) reached.`);
|
|
21
25
|
}
|
|
22
26
|
this.transformers.set(transformer.type, transformer);
|
|
23
27
|
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* using base64 encoding.
|
|
6
6
|
*/
|
|
7
7
|
import { SerializationTags } from "@zudojs/constants";
|
|
8
|
+
import { InvalidSerializedDataError } from "@zudojs/errors";
|
|
8
9
|
import { toBase64, fromBase64 } from "./encoding.utils.js";
|
|
9
10
|
const BUFFER_TYPE = "Buffer";
|
|
10
11
|
/** Transformer that handles Uint8Array round-trips. */
|
|
@@ -24,7 +25,7 @@ export const BufferTransformer = {
|
|
|
24
25
|
const data = value;
|
|
25
26
|
const raw = data[SerializationTags.VALUE];
|
|
26
27
|
if (typeof raw !== "string") {
|
|
27
|
-
throw new
|
|
28
|
+
throw new InvalidSerializedDataError(`Invalid Buffer serialized value: expected string, got ${typeof raw}`);
|
|
28
29
|
}
|
|
29
30
|
return fromBase64(raw);
|
|
30
31
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zudojs/serialization",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Data translation layer with JSON serializer, type transformers, envelopes, and registry for Zudojs applications.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -19,10 +19,10 @@
|
|
|
19
19
|
"LICENSE"
|
|
20
20
|
],
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@zudojs/constants": "1.
|
|
23
|
-
"@zudojs/errors": "1.0
|
|
24
|
-
"@zudojs/types": "1.
|
|
25
|
-
"@zudojs/validation": "1.0.
|
|
22
|
+
"@zudojs/constants": "1.1.1",
|
|
23
|
+
"@zudojs/errors": "1.2.0",
|
|
24
|
+
"@zudojs/types": "1.1.1",
|
|
25
|
+
"@zudojs/validation": "1.0.3"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"typescript": "7.0.2",
|