@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.
Files changed (27) hide show
  1. package/README.md +33 -5
  2. package/dist/index.d.ts +1 -1
  3. package/dist/index.js +1 -1
  4. package/dist/serializerEnvelope/envelope.core.d.ts +6 -2
  5. package/dist/serializerEnvelope/envelope.core.js +15 -10
  6. package/dist/serializerJson/index.d.ts +1 -0
  7. package/dist/serializerJson/index.js +1 -0
  8. package/dist/serializerJson/jsonSerializer.core.d.ts +13 -6
  9. package/dist/serializerJson/jsonSerializer.core.js +36 -131
  10. package/dist/serializerJson/jsonSerializer.escape.d.ts +35 -0
  11. package/dist/serializerJson/jsonSerializer.escape.js +50 -0
  12. package/dist/serializerJson/jsonSerializer.keys.d.ts +21 -0
  13. package/dist/serializerJson/jsonSerializer.keys.js +41 -0
  14. package/dist/serializerJson/jsonSerializer.restore.d.ts +17 -0
  15. package/dist/serializerJson/jsonSerializer.restore.js +86 -0
  16. package/dist/serializerJson/jsonSerializer.transform.d.ts +17 -0
  17. package/dist/serializerJson/jsonSerializer.transform.js +59 -0
  18. package/dist/serializerRegistry/serializerRegistry.factory.d.ts +9 -3
  19. package/dist/serializerRegistry/serializerRegistry.factory.js +7 -13
  20. package/dist/serializerTransforms/bigint.transformer.js +22 -8
  21. package/dist/serializerTransforms/date.transformer.js +6 -2
  22. package/dist/serializerTransforms/map.transformer.js +2 -1
  23. package/dist/serializerTransforms/set.transformer.js +2 -1
  24. package/dist/serializerTransforms/transformerRegistry.core.d.ts +5 -1
  25. package/dist/serializerTransforms/transformerRegistry.core.js +7 -3
  26. package/dist/serializerTransformsExt/buffer.transformer.js +2 -1
  27. 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
- The methods are `serialize` / `deserialize`, and every option can be overridden
26
- per call:
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
- - **An unrecognised `$type` tag is treated as ordinary data**, so a peer cannot
88
- stop the consumer with `{"$type":"anything"}` and your own records may carry a
89
- `$type` field. Pass `strict: true` to make an unknown tag an error instead.
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 {Error} when the shape or schema version is unusable.
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 {Error} when the envelope format doesn't match expectations.
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 {Error} when the shape or schema version is unusable.
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 Error(`Malformed envelope: expected an object, got ${envelope === null ? "null" : typeof envelope}`);
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 Error("Malformed envelope: missing metadata");
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 Error("Malformed envelope: metadata.format is missing");
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 Error("Malformed envelope: data must be a string or Uint8Array");
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 Error(`Malformed envelope: metadata.version must be an integer, got ${String(metadata.version)}`);
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 Error(`Unsupported envelope schema version ${metadata.version}: this build understands up to ${SERIALIZATION_SCHEMA_VERSION}`);
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 {Error} when the envelope format doesn't match expectations.
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 Error(`Envelope format mismatch: expected "${expectedFormat}", got "${envelope.metadata.format}"`);
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 Error(`Unsupported envelope encoding "${envelope.metadata.encoding}": only UTF-8 is supported`);
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
  }
@@ -5,4 +5,5 @@
5
5
  * with type preservation via transformer registry.
6
6
  */
7
7
  export { JSONSerializer } from "./jsonSerializer.core.js";
8
+ export { ESCAPED_OBJECT_TAG } from "./jsonSerializer.escape.js";
8
9
  //# sourceMappingURL=index.d.ts.map
@@ -5,4 +5,5 @@
5
5
  * with type preservation via transformer registry.
6
6
  */
7
7
  export { JSONSerializer } from "./jsonSerializer.core.js";
8
+ export { ESCAPED_OBJECT_TAG } from "./jsonSerializer.escape.js";
8
9
  //# sourceMappingURL=index.js.map
@@ -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
- /** Register a custom type transformer. */
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
- private transformValue;
25
- private restoreValue;
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, SerializationTags, SCHEMA_FORBIDDEN_KEYS, } from "@zudojs/constants";
12
- import { isPlainObject } from "@zudojs/types";
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
- /** Register a custom type transformer. */
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
- assertNoCircularReference(value);
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 = this.transformValue(value, 0, maxDepth, opts);
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
- this.assertOutputSize(json, maxSize);
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
- this.assertOutputSize(json, maxSize);
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
- this.assertInputSize(value, maxSize);
73
- if (opts.strict === true)
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 this.restoreValue(parsed, 0, maxDepth, opts);
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
- transformValue(value, depth, maxDepth, options) {
89
- if (value === null || value === undefined)
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 Error(`Invalid JSON: ${err.message}`);
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
- /** Options accepted when creating a serializer. */
13
- export interface CreateSerializerOptions {
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 accepted and thrown away.
17
- // They are now carried as per-instance defaults that each call can
18
- // still override.
19
- return new JSONSerializer({
20
- transformers: options?.transformers,
21
- defaults: {
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]: value.toString(),
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 Error(`Invalid BigInt serialized value: expected string, got ${typeof raw}`);
26
- }
27
- try {
28
- return BigInt(raw);
39
+ throw new InvalidSerializedDataError(`Invalid BigInt serialized value: expected string, got ${typeof raw}`);
29
40
  }
30
- catch {
31
- throw new Error(`Cannot parse BigInt from: "${raw}"`);
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 Error(`Invalid Date serialized value: expected string, got ${typeof raw}`);
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 Error(`Invalid Date value: "${raw}"`);
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 Error(`Invalid Map serialized value: expected array, got ${typeof raw}`);
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 Error(`Invalid Set serialized value: expected array, got ${typeof raw}`);
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
- /** Register a transformer. Throws if the registry is full. */
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
- /** Register a transformer. Throws if the registry is full. */
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 Error(`Maximum transformer limit (${SerializationLimits.MAX_TRANSFORMERS}) reached.`);
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 Error(`Invalid Buffer serialized value: expected string, got ${typeof raw}`);
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.0.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.0.1",
23
- "@zudojs/errors": "1.0.1",
24
- "@zudojs/types": "1.0.0",
25
- "@zudojs/validation": "1.0.1"
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",