@cqrs-ddd/safe-stringify 0.2.0

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.
@@ -0,0 +1,64 @@
1
+ /**
2
+ * A strictly JSON-serializable value (primitives, arrays, and plain records).
3
+ */
4
+ export type StrictJsonValue = null | boolean | number | string | StrictJsonValue[] | {
5
+ [key: string]: StrictJsonValue;
6
+ };
7
+ /**
8
+ * Normalizes a value to the strict, portable JSON domain used for deterministic
9
+ * hashing, idempotency fingerprints, and cache key generation.
10
+ *
11
+ * Enforces strict JSON boundaries:
12
+ * - Primitives (`string`, `boolean`, `null`, finite `number`) are preserved.
13
+ * - Dates are explicitly converted to ISO-8601 strings (`.toISOString()`).
14
+ * - Custom objects with `.toJSON()` methods are validated through their returned
15
+ * representation; internal fields excluded by that method are not inspected.
16
+ * - Object keys are optionally sorted recursively for deterministic serialization.
17
+ * - Unsupported types (non-finite numbers, `Map`, `Set`, `Error`, `RegExp`,
18
+ * binary buffers, promises, symbols, and sparse arrays) throw a {@link TypeError}.
19
+ * - Cyclic references are detected via a `WeakSet` and throw a {@link TypeError}.
20
+ *
21
+ * @param value The value to normalize.
22
+ * @param sortKeys Whether to sort object keys recursively in lexicographical order.
23
+ * @returns The strictly normalized JSON-compatible value.
24
+ * @throws {TypeError} If the value contains cycles or elements outside the JSON domain.
25
+ *
26
+ * @example
27
+ * ```ts
28
+ * const normalized = toStrictJsonValue({ b: 2, a: 1 }, true);
29
+ * // → { a: 1, b: 2 }
30
+ * ```
31
+ */
32
+ export declare function toStrictJsonValue(value: unknown, sortKeys?: boolean): StrictJsonValue;
33
+ /**
34
+ * Deterministically serializes a value to a JSON string with lexicographically
35
+ * sorted object keys, for cache keys, idempotency fingerprints and snapshots.
36
+ *
37
+ * Guarantees that structurally equal payloads produce byte-identical JSON strings
38
+ * regardless of property insertion order. The output forms stored keys, so it must
39
+ * never change: `golden-output.spec.ts` pins it.
40
+ *
41
+ * Enforces strict JSON boundaries, as {@link toStrictJsonValue} does:
42
+ * - Primitives (`string`, `boolean`, `null`, finite `number`) are preserved.
43
+ * - Dates are explicitly converted to ISO-8601 strings (`.toISOString()`).
44
+ * - Custom objects with `.toJSON()` methods are validated through their returned
45
+ * representation; internal fields excluded by that method are not inspected.
46
+ * - Object keys are sorted recursively.
47
+ * - Unsupported types (`undefined`, `bigint`, symbols, functions, non-finite
48
+ * numbers, `Map`, `Set`, `Error`, `RegExp`, binary buffers, promises,
49
+ * symbol-keyed properties and sparse arrays) throw a {@link TypeError}.
50
+ * - Cyclic references are detected via a `WeakSet` and throw a {@link TypeError}.
51
+ *
52
+ * @param value The value to serialize.
53
+ * @returns The deterministic JSON string representation.
54
+ * @throws {TypeError} If the value is cyclic, contains non-serializable types, or
55
+ * cannot be represented in JSON. The precise reason is the error's `cause`.
56
+ *
57
+ * @example
58
+ * ```ts
59
+ * stableStringify({ z: 1, a: 2 });
60
+ * // → '{"a":2,"z":1}'
61
+ * ```
62
+ */
63
+ export declare function stableStringify(value: unknown): string;
64
+ //# sourceMappingURL=stableStringify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stableStringify.d.ts","sourceRoot":"","sources":["../src/stableStringify.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,MAAM,MAAM,eAAe,GACvB,IAAI,GACJ,OAAO,GACP,MAAM,GACN,MAAM,GACN,eAAe,EAAE,GACjB;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,CAAA;CAAE,CAAC;AAEvC;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,OAAO,EACd,QAAQ,UAAQ,GACf,eAAe,CAEjB;AAoGD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAStD"}
@@ -0,0 +1,149 @@
1
+ "use strict";
2
+ /* Copyright (C) 2026-present Aristotelis — see repository license. */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.toStrictJsonValue = toStrictJsonValue;
5
+ exports.stableStringify = stableStringify;
6
+ /**
7
+ * Normalizes a value to the strict, portable JSON domain used for deterministic
8
+ * hashing, idempotency fingerprints, and cache key generation.
9
+ *
10
+ * Enforces strict JSON boundaries:
11
+ * - Primitives (`string`, `boolean`, `null`, finite `number`) are preserved.
12
+ * - Dates are explicitly converted to ISO-8601 strings (`.toISOString()`).
13
+ * - Custom objects with `.toJSON()` methods are validated through their returned
14
+ * representation; internal fields excluded by that method are not inspected.
15
+ * - Object keys are optionally sorted recursively for deterministic serialization.
16
+ * - Unsupported types (non-finite numbers, `Map`, `Set`, `Error`, `RegExp`,
17
+ * binary buffers, promises, symbols, and sparse arrays) throw a {@link TypeError}.
18
+ * - Cyclic references are detected via a `WeakSet` and throw a {@link TypeError}.
19
+ *
20
+ * @param value The value to normalize.
21
+ * @param sortKeys Whether to sort object keys recursively in lexicographical order.
22
+ * @returns The strictly normalized JSON-compatible value.
23
+ * @throws {TypeError} If the value contains cycles or elements outside the JSON domain.
24
+ *
25
+ * @example
26
+ * ```ts
27
+ * const normalized = toStrictJsonValue({ b: 2, a: 1 }, true);
28
+ * // → { a: 1, b: 2 }
29
+ * ```
30
+ */
31
+ function toStrictJsonValue(value, sortKeys = false) {
32
+ return normalize(value, new WeakSet(), sortKeys);
33
+ }
34
+ function normalize(value, ancestors, sortKeys) {
35
+ if (value === null ||
36
+ typeof value === 'string' ||
37
+ typeof value === 'boolean') {
38
+ return value;
39
+ }
40
+ if (typeof value === 'number') {
41
+ if (!Number.isFinite(value)) {
42
+ throw new TypeError('Non-finite numbers are not JSON-serializable.');
43
+ }
44
+ return value;
45
+ }
46
+ if (typeof value !== 'object') {
47
+ throw new TypeError(`Unsupported JSON value type: ${typeof value}.`);
48
+ }
49
+ if (ancestors.has(value)) {
50
+ throw new TypeError('Cyclic values are not JSON-serializable.');
51
+ }
52
+ if (value instanceof Date) {
53
+ if (Number.isNaN(value.getTime())) {
54
+ throw new TypeError('Invalid dates are not JSON-serializable.');
55
+ }
56
+ return value.toISOString();
57
+ }
58
+ if (isUnsupportedObject(value)) {
59
+ throw new TypeError(`${value.constructor?.name || 'Object'} is outside the supported JSON domain.`);
60
+ }
61
+ const toJSON = value.toJSON;
62
+ if (typeof toJSON === 'function') {
63
+ ancestors.add(value);
64
+ try {
65
+ return normalize(toJSON.call(value), ancestors, sortKeys);
66
+ }
67
+ finally {
68
+ ancestors.delete(value);
69
+ }
70
+ }
71
+ if (Object.getOwnPropertySymbols(value).length > 0) {
72
+ throw new TypeError('Symbol-keyed properties are outside the supported JSON domain.');
73
+ }
74
+ ancestors.add(value);
75
+ try {
76
+ if (Array.isArray(value)) {
77
+ const result = [];
78
+ for (let index = 0; index < value.length; index += 1) {
79
+ if (!(index in value)) {
80
+ throw new TypeError('Sparse arrays are not JSON-serializable.');
81
+ }
82
+ result.push(normalize(value[index], ancestors, sortKeys));
83
+ }
84
+ return result;
85
+ }
86
+ const source = value;
87
+ const keys = Object.keys(source);
88
+ if (sortKeys)
89
+ keys.sort();
90
+ const result = Object.create(null);
91
+ for (const key of keys) {
92
+ result[key] = normalize(source[key], ancestors, sortKeys);
93
+ }
94
+ return result;
95
+ }
96
+ finally {
97
+ ancestors.delete(value);
98
+ }
99
+ }
100
+ function isUnsupportedObject(value) {
101
+ return (value instanceof RegExp ||
102
+ value instanceof Error ||
103
+ value instanceof Map ||
104
+ value instanceof Set ||
105
+ value instanceof WeakMap ||
106
+ value instanceof WeakSet ||
107
+ value instanceof ArrayBuffer ||
108
+ ArrayBuffer.isView(value) ||
109
+ value instanceof Promise);
110
+ }
111
+ /**
112
+ * Deterministically serializes a value to a JSON string with lexicographically
113
+ * sorted object keys, for cache keys, idempotency fingerprints and snapshots.
114
+ *
115
+ * Guarantees that structurally equal payloads produce byte-identical JSON strings
116
+ * regardless of property insertion order. The output forms stored keys, so it must
117
+ * never change: `golden-output.spec.ts` pins it.
118
+ *
119
+ * Enforces strict JSON boundaries, as {@link toStrictJsonValue} does:
120
+ * - Primitives (`string`, `boolean`, `null`, finite `number`) are preserved.
121
+ * - Dates are explicitly converted to ISO-8601 strings (`.toISOString()`).
122
+ * - Custom objects with `.toJSON()` methods are validated through their returned
123
+ * representation; internal fields excluded by that method are not inspected.
124
+ * - Object keys are sorted recursively.
125
+ * - Unsupported types (`undefined`, `bigint`, symbols, functions, non-finite
126
+ * numbers, `Map`, `Set`, `Error`, `RegExp`, binary buffers, promises,
127
+ * symbol-keyed properties and sparse arrays) throw a {@link TypeError}.
128
+ * - Cyclic references are detected via a `WeakSet` and throw a {@link TypeError}.
129
+ *
130
+ * @param value The value to serialize.
131
+ * @returns The deterministic JSON string representation.
132
+ * @throws {TypeError} If the value is cyclic, contains non-serializable types, or
133
+ * cannot be represented in JSON. The precise reason is the error's `cause`.
134
+ *
135
+ * @example
136
+ * ```ts
137
+ * stableStringify({ z: 1, a: 2 });
138
+ * // → '{"a":2,"z":1}'
139
+ * ```
140
+ */
141
+ function stableStringify(value) {
142
+ try {
143
+ return JSON.stringify(toStrictJsonValue(value, true));
144
+ }
145
+ catch (cause) {
146
+ throw new TypeError('stableStringify requires an acyclic JSON-serializable value.', { cause });
147
+ }
148
+ }
149
+ //# sourceMappingURL=stableStringify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stableStringify.js","sourceRoot":"","sources":["../src/stableStringify.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@cqrs-ddd/safe-stringify",
3
+ "version": "0.2.0",
4
+ "description": "Deterministic JSON for identity (cache keys, fingerprints) and safe, redacting JSON for logs, with no dependencies and no framework.",
5
+ "author": "Aristotelis",
6
+ "license": "SEE LICENSE IN LICENSE",
7
+ "engines": {
8
+ "node": ">=22.0.0"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/aristoteliss/nestjs-pipeline.git",
13
+ "directory": "packages/safe-stringify"
14
+ },
15
+ "homepage": "https://github.com/aristoteliss/nestjs-pipeline/tree/master/packages/safe-stringify#readme",
16
+ "bugs": {
17
+ "url": "https://github.com/aristoteliss/nestjs-pipeline/issues"
18
+ },
19
+ "keywords": [
20
+ "json",
21
+ "stable-stringify",
22
+ "deterministic",
23
+ "redaction",
24
+ "cache-key"
25
+ ],
26
+ "main": "dist/index.js",
27
+ "types": "dist/index.d.ts",
28
+ "files": [
29
+ "dist",
30
+ "LICENSE",
31
+ "COMMERCIAL_LICENSE.txt"
32
+ ],
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "devDependencies": {
37
+ "@vitest/coverage-v8": "^4.1.11",
38
+ "vitest": "^4.1.11"
39
+ },
40
+ "scripts": {
41
+ "build": "tsc -p tsconfig.build.json",
42
+ "build:watch": "tsc -p tsconfig.build.json --watch",
43
+ "clean": "rm -rf dist tsconfig.tsbuildinfo",
44
+ "rebuild": "pnpm run clean && pnpm run build",
45
+ "lint": "tsc --noEmit",
46
+ "test": "vitest run",
47
+ "test:watch": "vitest"
48
+ }
49
+ }