@zudojs/serialization 1.1.0 → 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.
@@ -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
  }
@@ -56,7 +56,11 @@ export class JSONSerializer {
56
56
  const maxDepth = opts.maxDepth ?? SerializationLimits.MAX_DEPTH;
57
57
  const maxSize = opts.maxSize ?? SerializationLimits.MAX_SIZE;
58
58
  if (opts.preserveTypes === true) {
59
- 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);
60
64
  assertDepthWithinLimit(value, maxDepth);
61
65
  const transformed = transformValue({ transformers: this.transformers, maxDepth, options: opts }, value, 0);
62
66
  const json = opts.pretty
@@ -5,10 +5,18 @@
5
5
  * plain objects (see `jsonSerializer.escape.ts`).
6
6
  */
7
7
  import { InvalidSerializedDataError, SerializationDepthError, TransformerError, isSerializationError, } from "@zudojs/errors";
8
- import { SerializationTags } from "@zudojs/constants";
8
+ import { SerializationLimits, SerializationTags } from "@zudojs/constants";
9
9
  import { isPlainObject } from "@zudojs/types";
10
10
  import { escapedBody } from "./jsonSerializer.escape.js";
11
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
+ }
12
20
  /** Rebuilds runtime values from their tagged JSON representation. */
13
21
  export function restoreValue(walk, value, depth) {
14
22
  if (value === null || typeof value !== "object")
@@ -29,11 +37,19 @@ export function restoreValue(walk, value, depth) {
29
37
  }
30
38
  const tag = obj[SerializationTags.TYPE];
31
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
+ }
32
48
  if (walk.transformers.has(tag))
33
49
  return revive(walk, tag, obj, depth);
34
50
  // An unknown tag is ordinary data unless the caller asked for strictness.
35
51
  if (strict) {
36
- throw new InvalidSerializedDataError(`Unknown serialization type tag: "${tag}". ` +
52
+ throw new InvalidSerializedDataError(`Unknown serialization type tag: "${describeTag(tag)}". ` +
37
53
  "Register a transformer for it, or deserialize without strict mode.", { format: "json" });
38
54
  }
39
55
  }
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zudojs/serialization",
3
- "version": "1.1.0",
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.1.0",
23
- "@zudojs/errors": "1.1.0",
24
- "@zudojs/types": "1.1.0",
25
- "@zudojs/validation": "1.0.2"
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",