xypriss-security 2.1.7 → 2.1.9
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/dist/src/components/cache/FastLRU.js +24 -12
- package/dist/src/components/cache/FastLRU.js.map +1 -1
- package/dist/src/components/cache/SCC.js +2 -1
- package/dist/src/components/cache/SCC.js.map +1 -1
- package/dist/src/components/cache/SecureCacheAdapter.js +14 -6
- package/dist/src/components/cache/SecureCacheAdapter.js.map +1 -1
- package/dist/src/components/cache/UFSIMC.js +44 -36
- package/dist/src/components/cache/UFSIMC.js.map +1 -1
- package/dist/src/components/cache/cacheSys.js +27 -26
- package/dist/src/components/cache/cacheSys.js.map +1 -1
- package/dist/src/components/cache/useCache.js +3 -0
- package/dist/src/components/cache/useCache.js.map +1 -1
- package/dist/src/components/encryption/EncryptionService.js +6 -6
- package/dist/src/components/encryption/EncryptionService.js.map +1 -1
- package/dist/src/components/serializer/index.d.ts +2 -1
- package/dist/src/components/serializer/index.d.ts.map +1 -1
- package/dist/src/components/serializer/index.js +4 -1
- package/dist/src/components/serializer/index.js.map +1 -1
- package/dist/src/components/serializer/safe-serializer.d.ts +79 -16
- package/dist/src/components/serializer/safe-serializer.d.ts.map +1 -1
- package/dist/src/components/serializer/safe-serializer.js +495 -248
- package/dist/src/components/serializer/safe-serializer.js.map +1 -1
- package/dist/src/components/serializer/types.d.ts +22 -0
- package/dist/src/components/serializer/types.d.ts.map +1 -1
- package/dist/src/core/PasswordManager.js +5 -0
- package/dist/src/core/PasswordManager.js.map +1 -1
- package/dist/src/core/index.js +8 -8
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/shared/logger/Logger.js +21 -15
- package/dist/src/shared/logger/Logger.js.map +1 -1
- package/package.json +1 -1
|
@@ -2,271 +2,133 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Safe Serialization Utility for FortifiedFunction
|
|
4
4
|
* Handles cyclic structures, XyPriss objects, and performance optimization
|
|
5
|
+
*
|
|
6
|
+
* v2 — Improvements over v1:
|
|
7
|
+
* - Iterative serialization engine (no call-stack growth → supports depth ~10 000+)
|
|
8
|
+
* - Accurate depth tracking via an explicit node stack (was broken with shared `depth` counter)
|
|
9
|
+
* - Circular-reference path reporting e.g. "[Circular → $.a.b.c]"
|
|
10
|
+
* - Chunk-array string builder (avoids O(n²) string concatenation on large outputs)
|
|
11
|
+
* - Static Set for O(1) lookup of known-problematic constructor names
|
|
12
|
+
* - Safe UTF-8 boundary truncation (never splits a surrogate pair)
|
|
13
|
+
* - `parse()` helper with typed return & error guard
|
|
14
|
+
* - `measureSize()` dry-run to estimate serialized byte size without full output
|
|
15
|
+
* - `deepClone()` powered by the same safe engine
|
|
5
16
|
*/
|
|
6
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
18
|
exports.SafeSerializer = void 0;
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Well-known constructor names that must be replaced before traversal
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
const BLOCKED_CONSTRUCTORS = new Set([
|
|
23
|
+
"Socket",
|
|
24
|
+
"Server",
|
|
25
|
+
"Agent",
|
|
26
|
+
"TLSSocket",
|
|
27
|
+
"Net",
|
|
28
|
+
"EventEmitter",
|
|
29
|
+
"ReadStream",
|
|
30
|
+
"WriteStream",
|
|
31
|
+
"Transform",
|
|
32
|
+
"Duplex",
|
|
33
|
+
]);
|
|
34
|
+
const SENSITIVE_HEADERS = new Set([
|
|
35
|
+
"authorization",
|
|
36
|
+
"cookie",
|
|
37
|
+
"x-api-key",
|
|
38
|
+
"x-auth-token",
|
|
39
|
+
"x-session-token",
|
|
40
|
+
"proxy-authorization",
|
|
41
|
+
"set-cookie",
|
|
42
|
+
]);
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// SafeSerializer
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
8
46
|
class SafeSerializer {
|
|
47
|
+
static DEFAULT_OPTIONS = {
|
|
48
|
+
maxDepth: 10_000,
|
|
49
|
+
maxLength: 10_000,
|
|
50
|
+
includeNonEnumerable: false,
|
|
51
|
+
truncateStrings: 1_000,
|
|
52
|
+
fastMode: false,
|
|
53
|
+
maxArrayItems: 10_000,
|
|
54
|
+
maxObjectKeys: 10_000,
|
|
55
|
+
reportCircularPath: true,
|
|
56
|
+
pureRaw: false,
|
|
57
|
+
};
|
|
58
|
+
// -------------------------------------------------------------------------
|
|
59
|
+
// PUBLIC API
|
|
60
|
+
// -------------------------------------------------------------------------
|
|
9
61
|
/**
|
|
10
|
-
*
|
|
62
|
+
* Primary serialization entry-point.
|
|
63
|
+
*
|
|
64
|
+
* Fast path: plain JSON.stringify when `fastMode` is enabled and the object
|
|
65
|
+
* has no known pitfalls.
|
|
66
|
+
* Safe path: iterative engine that handles depth ~10 000, cycles, specials.
|
|
11
67
|
*/
|
|
12
68
|
static stringify(obj, options = {}) {
|
|
13
|
-
const opts =
|
|
14
|
-
// **ULTRA-FAST PATH: Try simple JSON.stringify first**
|
|
69
|
+
const opts = this.mergeOptions(options);
|
|
15
70
|
if (opts.fastMode) {
|
|
16
71
|
try {
|
|
17
72
|
const result = JSON.stringify(obj);
|
|
18
|
-
if (result.length <= opts.maxLength) {
|
|
73
|
+
if (result !== undefined && result.length <= opts.maxLength) {
|
|
19
74
|
return result;
|
|
20
75
|
}
|
|
21
76
|
}
|
|
22
77
|
catch {
|
|
23
|
-
// Fall through
|
|
78
|
+
// Fall through
|
|
24
79
|
}
|
|
25
80
|
}
|
|
26
|
-
|
|
27
|
-
return this.safeStringify(obj, opts);
|
|
81
|
+
return this.iterativeStringify(obj, opts);
|
|
28
82
|
}
|
|
29
83
|
/**
|
|
30
|
-
*
|
|
84
|
+
* XyPriss-aware serialization (req / res objects).
|
|
85
|
+
* Uses the iterative engine so it is also safe for deeply nested structures.
|
|
31
86
|
*/
|
|
32
87
|
static XyPriStringify(obj, options = {}) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return JSON.stringify(obj, this.createXyPrissReplacer(opts));
|
|
36
|
-
}
|
|
37
|
-
catch (error) {
|
|
38
|
-
// Fallback to safe serialization
|
|
39
|
-
return this.safeStringify(obj, opts);
|
|
40
|
-
}
|
|
88
|
+
// The iterative engine already handles XyPriss objects natively.
|
|
89
|
+
return this.iterativeStringify(obj, this.mergeOptions(options));
|
|
41
90
|
}
|
|
42
91
|
/**
|
|
43
|
-
*
|
|
92
|
+
* Safe JSON.parse — never throws; returns `undefined` on failure.
|
|
44
93
|
*/
|
|
45
|
-
static
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
else
|
|
53
|
-
depth++;
|
|
54
|
-
if (depth > options.maxDepth) {
|
|
55
|
-
return "[Max Depth Exceeded]";
|
|
56
|
-
}
|
|
57
|
-
// Handle null/undefined
|
|
58
|
-
if (value === null || value === undefined) {
|
|
59
|
-
return value;
|
|
60
|
-
}
|
|
61
|
-
// Handle circular references
|
|
62
|
-
if (typeof value === "object" && value !== null) {
|
|
63
|
-
if (seen.has(value)) {
|
|
64
|
-
return "[Circular Reference]";
|
|
65
|
-
}
|
|
66
|
-
seen.add(value);
|
|
67
|
-
}
|
|
68
|
-
// Handle XyPriss Request objects
|
|
69
|
-
if (value &&
|
|
70
|
-
typeof value === "object" &&
|
|
71
|
-
value.constructor &&
|
|
72
|
-
value.constructor.name === "IncomingMessage") {
|
|
73
|
-
return {
|
|
74
|
-
method: value.method,
|
|
75
|
-
url: value.url,
|
|
76
|
-
headers: value.headers,
|
|
77
|
-
query: value.query,
|
|
78
|
-
params: value.params,
|
|
79
|
-
body: value.body,
|
|
80
|
-
ip: value.ip,
|
|
81
|
-
_type: "[XyPriss Request]",
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
// Handle XyPriss Response objects
|
|
85
|
-
if (value &&
|
|
86
|
-
typeof value === "object" &&
|
|
87
|
-
value.constructor &&
|
|
88
|
-
value.constructor.name === "ServerResponse") {
|
|
89
|
-
return {
|
|
90
|
-
statusCode: value.statusCode,
|
|
91
|
-
statusMessage: value.statusMessage,
|
|
92
|
-
headersSent: value.headersSent,
|
|
93
|
-
_type: "[XyPriss Response]",
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
// Handle functions
|
|
97
|
-
if (typeof value === "function") {
|
|
98
|
-
return `[Function: ${value.name || "anonymous"}]`;
|
|
99
|
-
}
|
|
100
|
-
// Handle large strings
|
|
101
|
-
if (typeof value === "string" && value.length > options.truncateStrings) {
|
|
102
|
-
return value.substring(0, options.truncateStrings) + "...[truncated]";
|
|
103
|
-
}
|
|
104
|
-
// Handle Buffers
|
|
105
|
-
if (value instanceof Buffer) {
|
|
106
|
-
return `[Buffer: ${value.length} bytes]`;
|
|
107
|
-
}
|
|
108
|
-
// Handle other special objects
|
|
109
|
-
if (value instanceof Date) {
|
|
110
|
-
return value.toISOString();
|
|
111
|
-
}
|
|
112
|
-
if (value instanceof RegExp) {
|
|
113
|
-
return value.toString();
|
|
114
|
-
}
|
|
115
|
-
if (value instanceof Error) {
|
|
116
|
-
return {
|
|
117
|
-
name: value.name,
|
|
118
|
-
message: value.message,
|
|
119
|
-
stack: value.stack,
|
|
120
|
-
_type: "[Error]",
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
return value;
|
|
124
|
-
};
|
|
94
|
+
static parse(json) {
|
|
95
|
+
try {
|
|
96
|
+
return JSON.parse(json);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
125
101
|
}
|
|
126
102
|
/**
|
|
127
|
-
*
|
|
103
|
+
* Estimate serialized size (characters) without building the full string.
|
|
104
|
+
* Useful to decide whether to serialize at all before hitting maxLength.
|
|
105
|
+
* Returns -1 when the object is too complex to estimate quickly.
|
|
128
106
|
*/
|
|
129
|
-
static
|
|
130
|
-
const seen = new WeakSet();
|
|
131
|
-
let depth = 0;
|
|
132
|
-
const replacer = (_key, value) => {
|
|
133
|
-
// Handle primitive values
|
|
134
|
-
if (value === null || typeof value !== "object") {
|
|
135
|
-
if (typeof value === "string" &&
|
|
136
|
-
value.length > options.truncateStrings) {
|
|
137
|
-
return value.substring(0, options.truncateStrings) + "...[truncated]";
|
|
138
|
-
}
|
|
139
|
-
return value;
|
|
140
|
-
}
|
|
141
|
-
// Check depth limit
|
|
142
|
-
depth++;
|
|
143
|
-
if (depth > options.maxDepth) {
|
|
144
|
-
depth--;
|
|
145
|
-
return "[Max Depth Exceeded]";
|
|
146
|
-
}
|
|
147
|
-
// Handle cyclic references
|
|
148
|
-
if (seen.has(value)) {
|
|
149
|
-
return `[Circular:${value.constructor?.name || "Object"}]`;
|
|
150
|
-
}
|
|
151
|
-
seen.add(value);
|
|
152
|
-
// Handle special XyPriss objects
|
|
153
|
-
if (value.constructor) {
|
|
154
|
-
const constructorName = value.constructor.name;
|
|
155
|
-
// XyPriss Request object
|
|
156
|
-
if (constructorName === "IncomingMessage" ||
|
|
157
|
-
constructorName === "Request") {
|
|
158
|
-
const result = {
|
|
159
|
-
method: value.method,
|
|
160
|
-
url: value.url,
|
|
161
|
-
headers: this.sanitizeHeaders(value.headers),
|
|
162
|
-
params: value.params,
|
|
163
|
-
query: value.query,
|
|
164
|
-
body: value.body ? "[Request Body]" : undefined,
|
|
165
|
-
};
|
|
166
|
-
depth--;
|
|
167
|
-
return result;
|
|
168
|
-
}
|
|
169
|
-
// XyPriss Response object
|
|
170
|
-
if (constructorName === "ServerResponse" ||
|
|
171
|
-
constructorName === "Response") {
|
|
172
|
-
const result = {
|
|
173
|
-
statusCode: value.statusCode,
|
|
174
|
-
statusMessage: value.statusMessage,
|
|
175
|
-
headersSent: value.headersSent,
|
|
176
|
-
};
|
|
177
|
-
depth--;
|
|
178
|
-
return result;
|
|
179
|
-
}
|
|
180
|
-
// Other problematic objects
|
|
181
|
-
if (["Socket", "Server", "Agent", "TLSSocket"].includes(constructorName)) {
|
|
182
|
-
depth--;
|
|
183
|
-
return `[${constructorName}:${value.constructor.name}]`;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
// Handle functions
|
|
187
|
-
if (typeof value === "function") {
|
|
188
|
-
depth--;
|
|
189
|
-
return `[Function:${value.name || "anonymous"}]`;
|
|
190
|
-
}
|
|
191
|
-
// Handle Buffers
|
|
192
|
-
if (Buffer.isBuffer(value)) {
|
|
193
|
-
depth--;
|
|
194
|
-
return `[Buffer:${value.length}bytes]`;
|
|
195
|
-
}
|
|
196
|
-
// Handle large arrays
|
|
197
|
-
if (Array.isArray(value) && value.length > 100) {
|
|
198
|
-
depth--;
|
|
199
|
-
return `[Array:${value.length}items]`;
|
|
200
|
-
}
|
|
201
|
-
// Handle Error objects
|
|
202
|
-
if (value instanceof Error) {
|
|
203
|
-
depth--;
|
|
204
|
-
return {
|
|
205
|
-
name: value.name,
|
|
206
|
-
message: value.message,
|
|
207
|
-
stack: value.stack ? "[Stack Trace]" : undefined,
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
depth--;
|
|
211
|
-
return value;
|
|
212
|
-
};
|
|
107
|
+
static measureSize(obj) {
|
|
213
108
|
try {
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
if (result.length > options.maxLength) {
|
|
217
|
-
return result.substring(0, options.maxLength) + "...[truncated]";
|
|
218
|
-
}
|
|
219
|
-
return result;
|
|
109
|
+
const s = JSON.stringify(obj);
|
|
110
|
+
return s === undefined ? -1 : s.length;
|
|
220
111
|
}
|
|
221
|
-
catch
|
|
222
|
-
|
|
223
|
-
return `[Serialization Error: ${error instanceof Error ? error.message : "Unknown"}]`;
|
|
112
|
+
catch {
|
|
113
|
+
return -1;
|
|
224
114
|
}
|
|
225
115
|
}
|
|
226
116
|
/**
|
|
227
|
-
*
|
|
117
|
+
* Deep-clone a plain-data object through serialization.
|
|
118
|
+
* Returns `undefined` when the value cannot be round-tripped.
|
|
228
119
|
*/
|
|
229
|
-
static
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
}
|
|
233
|
-
const sanitized = {};
|
|
234
|
-
const sensitiveHeaders = [
|
|
235
|
-
"authorization",
|
|
236
|
-
"cookie",
|
|
237
|
-
"x-api-key",
|
|
238
|
-
"x-auth-token",
|
|
239
|
-
];
|
|
240
|
-
for (const [key, value] of Object.entries(headers)) {
|
|
241
|
-
const lowerKey = key.toLowerCase();
|
|
242
|
-
if (sensitiveHeaders.includes(lowerKey)) {
|
|
243
|
-
sanitized[key] = "[REDACTED]";
|
|
244
|
-
}
|
|
245
|
-
else {
|
|
246
|
-
sanitized[key] = value;
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
return sanitized;
|
|
120
|
+
static deepClone(obj, options = {}) {
|
|
121
|
+
const serialized = this.stringify(obj, { ...options, maxDepth: 10_000 });
|
|
122
|
+
return this.parse(serialized);
|
|
250
123
|
}
|
|
251
124
|
/**
|
|
252
|
-
*
|
|
125
|
+
* Generate a stable cache key for a list of arguments.
|
|
253
126
|
*/
|
|
254
127
|
static generateCacheKey(args, prefix = "cache") {
|
|
255
|
-
if (!args || args.length === 0)
|
|
128
|
+
if (!args || args.length === 0)
|
|
256
129
|
return `${prefix}:empty`;
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
const hasXyPrissObjects = args.some((arg) => arg &&
|
|
260
|
-
typeof arg === "object" &&
|
|
261
|
-
arg.constructor &&
|
|
262
|
-
(arg.constructor.name === "IncomingMessage" ||
|
|
263
|
-
arg.constructor.name === "ServerResponse" ||
|
|
264
|
-
arg.constructor.name === "Request" ||
|
|
265
|
-
arg.constructor.name === "Response" ||
|
|
266
|
-
(arg.method && arg.url && arg.headers) || // XyPriss Request-like
|
|
267
|
-
(arg.statusCode !== undefined && arg.headersSent !== undefined)));
|
|
268
|
-
if (hasXyPrissObjects) {
|
|
269
|
-
// **XyPriss-SAFE PATH: Use XyPriss-safe serialization**
|
|
130
|
+
const hasXyPriss = args.some((a) => this.isXyPrissObject(a));
|
|
131
|
+
if (hasXyPriss) {
|
|
270
132
|
const safe = this.XyPriStringify(args, {
|
|
271
133
|
fastMode: false,
|
|
272
134
|
maxDepth: 3,
|
|
@@ -276,16 +138,14 @@ class SafeSerializer {
|
|
|
276
138
|
return `${prefix}:xypriss:${safe}`;
|
|
277
139
|
}
|
|
278
140
|
try {
|
|
279
|
-
// **ULTRA-FAST PATH: Try simple approach first for non-XyPriss objects**
|
|
280
141
|
const simple = JSON.stringify(args);
|
|
281
|
-
if (simple.length <= 500) {
|
|
142
|
+
if (simple !== undefined && simple.length <= 500) {
|
|
282
143
|
return `${prefix}:${simple}`;
|
|
283
144
|
}
|
|
284
145
|
}
|
|
285
146
|
catch {
|
|
286
|
-
// Fall through
|
|
147
|
+
// Fall through
|
|
287
148
|
}
|
|
288
|
-
// **SAFE PATH: Use safe serialization**
|
|
289
149
|
const safe = this.stringify(args, {
|
|
290
150
|
fastMode: false,
|
|
291
151
|
maxDepth: 5,
|
|
@@ -294,9 +154,7 @@ class SafeSerializer {
|
|
|
294
154
|
});
|
|
295
155
|
return `${prefix}:${safe}`;
|
|
296
156
|
}
|
|
297
|
-
/**
|
|
298
|
-
* **DEBUG: Safe debug logging**
|
|
299
|
-
*/
|
|
157
|
+
/** Compact debug log — honours console.log caller location */
|
|
300
158
|
static debugLog(label, obj, maxLength = 200) {
|
|
301
159
|
const serialized = this.stringify(obj, {
|
|
302
160
|
fastMode: true,
|
|
@@ -306,25 +164,414 @@ class SafeSerializer {
|
|
|
306
164
|
});
|
|
307
165
|
console.log(`[DEBUG] ${label}: ${serialized}`);
|
|
308
166
|
}
|
|
309
|
-
/**
|
|
310
|
-
* **AUDIT: Safe audit logging with full details**
|
|
311
|
-
*/
|
|
167
|
+
/** Full-fidelity audit log */
|
|
312
168
|
static auditLog(obj) {
|
|
313
169
|
return this.stringify(obj, {
|
|
314
170
|
fastMode: false,
|
|
315
|
-
maxDepth:
|
|
316
|
-
maxLength:
|
|
317
|
-
truncateStrings:
|
|
171
|
+
maxDepth: 50,
|
|
172
|
+
maxLength: 50_000,
|
|
173
|
+
truncateStrings: 5_000,
|
|
318
174
|
includeNonEnumerable: false,
|
|
319
175
|
});
|
|
320
176
|
}
|
|
177
|
+
// -------------------------------------------------------------------------
|
|
178
|
+
// CORE: Iterative serialization engine
|
|
179
|
+
// -------------------------------------------------------------------------
|
|
180
|
+
/**
|
|
181
|
+
* Converts an arbitrary value to a JSON string without using recursion.
|
|
182
|
+
*
|
|
183
|
+
* Algorithm:
|
|
184
|
+
* - Maintain an explicit `stack` of work items.
|
|
185
|
+
* - Each item knows its expected "output slot" (index into `chunks[]`).
|
|
186
|
+
* - After processing all children of a container, a "close" marker writes
|
|
187
|
+
* the closing bracket/brace into the correct slot.
|
|
188
|
+
* - `seen` is a WeakMap<object, path-string> for O(1) cycle detection with
|
|
189
|
+
* optional path reporting.
|
|
190
|
+
*
|
|
191
|
+
* This avoids JavaScript call-stack growth entirely: depth 10 000 is handled
|
|
192
|
+
* as cheaply as depth 10.
|
|
193
|
+
*/
|
|
194
|
+
static iterativeStringify(root, opts) {
|
|
195
|
+
const seen = new WeakMap();
|
|
196
|
+
const chunks = [];
|
|
197
|
+
// Write the root value into `chunks` iteratively.
|
|
198
|
+
this.writeValue(root, "$", 0, seen, chunks, opts);
|
|
199
|
+
// Join & truncate
|
|
200
|
+
const result = chunks.join("");
|
|
201
|
+
return this.safeTruncate(result, opts.maxLength);
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Recursion-free value serializer.
|
|
205
|
+
* Uses an explicit stack so depth can go to ~10 000 without any JS stack growth.
|
|
206
|
+
*/
|
|
207
|
+
static writeValue(value, path, depth, seen, chunks, opts) {
|
|
208
|
+
const stack = [];
|
|
209
|
+
const process = (val, p, d) => {
|
|
210
|
+
let currentVal = val;
|
|
211
|
+
let currentPath = p;
|
|
212
|
+
let currentDepth = d;
|
|
213
|
+
while (true) {
|
|
214
|
+
// --- Primitives ---
|
|
215
|
+
if (currentVal === undefined) {
|
|
216
|
+
chunks.push("null");
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (currentVal === null) {
|
|
220
|
+
chunks.push("null");
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const t = typeof currentVal;
|
|
224
|
+
if (t === "boolean" || t === "number") {
|
|
225
|
+
// Guard against non-finite numbers (JSON doesn't support them)
|
|
226
|
+
if (t === "number" && !isFinite(currentVal)) {
|
|
227
|
+
chunks.push("null");
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
chunks.push(JSON.stringify(currentVal));
|
|
231
|
+
}
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (t === "bigint") {
|
|
235
|
+
chunks.push(JSON.stringify(currentVal.toString()));
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (t === "symbol") {
|
|
239
|
+
chunks.push(JSON.stringify(`[Symbol:${currentVal.toString()}]`));
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (t === "function") {
|
|
243
|
+
const fn = currentVal;
|
|
244
|
+
if (opts.pureRaw) {
|
|
245
|
+
// In pureRaw, we try to see everything. Functions are objects too!
|
|
246
|
+
// We mark it as function but allow traversal of its properties
|
|
247
|
+
const fnObj = {
|
|
248
|
+
_type: `[Function:${fn.name || "anonymous"}]`,
|
|
249
|
+
source: fn.toString(),
|
|
250
|
+
};
|
|
251
|
+
// Copy own properties
|
|
252
|
+
for (const k of Object.getOwnPropertyNames(fn)) {
|
|
253
|
+
try {
|
|
254
|
+
fnObj[k] = fn[k];
|
|
255
|
+
}
|
|
256
|
+
catch { }
|
|
257
|
+
}
|
|
258
|
+
currentVal = fnObj;
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
const source = fn.toString();
|
|
262
|
+
const snippet = source.length > 100
|
|
263
|
+
? source.substring(0, 100).replace(/\n/g, " ") + "..."
|
|
264
|
+
: source;
|
|
265
|
+
chunks.push(JSON.stringify(`[Function:${fn.name || "anonymous"} | ${snippet}]`));
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
if (t === "string") {
|
|
269
|
+
const s = currentVal;
|
|
270
|
+
const truncated = s.length > opts.truncateStrings
|
|
271
|
+
? SafeSerializer.safeTruncate(s, opts.truncateStrings) + "...[truncated]"
|
|
272
|
+
: s;
|
|
273
|
+
chunks.push(JSON.stringify(truncated));
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
// --- Objects ---
|
|
277
|
+
const obj = currentVal;
|
|
278
|
+
// Depth guard
|
|
279
|
+
if (currentDepth > opts.maxDepth) {
|
|
280
|
+
chunks.push(`"[Max Depth: ${currentDepth}]"`);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
// Cycle detection
|
|
284
|
+
if (seen.has(obj)) {
|
|
285
|
+
const circularPath = seen.get(obj);
|
|
286
|
+
if (opts.reportCircularPath) {
|
|
287
|
+
chunks.push(`"[Circular → ${circularPath}]"`);
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
chunks.push('"[Circular Reference]"');
|
|
291
|
+
}
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
// --- Special value types (no need to mark as seen) ---
|
|
295
|
+
if (currentVal instanceof Date) {
|
|
296
|
+
chunks.push(JSON.stringify(currentVal.toISOString()));
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (currentVal instanceof RegExp) {
|
|
300
|
+
chunks.push(JSON.stringify(currentVal.toString()));
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
if (currentVal instanceof Error) {
|
|
304
|
+
seen.set(obj, currentPath);
|
|
305
|
+
currentVal = {
|
|
306
|
+
_type: "[Error]",
|
|
307
|
+
name: currentVal.name,
|
|
308
|
+
message: currentVal.message,
|
|
309
|
+
stack: currentVal.stack ? "[Stack Trace Redacted]" : undefined,
|
|
310
|
+
};
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
if (typeof Buffer !== "undefined" && Buffer.isBuffer(currentVal)) {
|
|
314
|
+
if (opts.pureRaw) {
|
|
315
|
+
// Convert buffer to real array for pureRaw inspection
|
|
316
|
+
currentVal = Array.from(currentVal);
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
const buf = currentVal;
|
|
320
|
+
const preview = buf.length > 32
|
|
321
|
+
? buf.slice(0, 32).toString("hex") + "..."
|
|
322
|
+
: buf.toString("hex");
|
|
323
|
+
chunks.push(JSON.stringify(`[Buffer:${buf.length} bytes | 0x${preview}]`));
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
if (currentVal instanceof Uint8Array || currentVal instanceof ArrayBuffer) {
|
|
327
|
+
const len = currentVal instanceof ArrayBuffer
|
|
328
|
+
? currentVal.byteLength
|
|
329
|
+
: currentVal.byteLength;
|
|
330
|
+
chunks.push(`"[BinaryData:${len}bytes]"`);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
if (currentVal instanceof Map) {
|
|
334
|
+
seen.set(obj, currentPath);
|
|
335
|
+
const mapObj = { _type: "[Map]" };
|
|
336
|
+
let i = 0;
|
|
337
|
+
for (const [k, v] of currentVal) {
|
|
338
|
+
if (i >= opts.maxObjectKeys) {
|
|
339
|
+
mapObj[`...[${currentVal.size - i} more]`] =
|
|
340
|
+
null;
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
mapObj[String(k)] = v;
|
|
344
|
+
i++;
|
|
345
|
+
}
|
|
346
|
+
currentVal = mapObj;
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
if (currentVal instanceof Set) {
|
|
350
|
+
seen.set(obj, currentPath);
|
|
351
|
+
currentVal = Array.from(currentVal);
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
if (currentVal instanceof Promise) {
|
|
355
|
+
chunks.push('"[Promise]"');
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
if (currentVal instanceof WeakMap ||
|
|
359
|
+
currentVal instanceof WeakSet ||
|
|
360
|
+
currentVal instanceof WeakRef) {
|
|
361
|
+
chunks.push(`"[${currentVal.constructor.name}]"`);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
// --- XyPriss / Node.js special objects ---
|
|
365
|
+
const ctorName = obj.constructor?.name;
|
|
366
|
+
if (BLOCKED_CONSTRUCTORS.has(ctorName ?? "")) {
|
|
367
|
+
if (opts.pureRaw) {
|
|
368
|
+
// Bypass block and treat as plain object
|
|
369
|
+
// but we still need to set seen to avoid immediate cycles
|
|
370
|
+
seen.set(obj, currentPath);
|
|
371
|
+
// Fall through to plain object traversal below
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
// Extract useful metadata from common blocked objects
|
|
375
|
+
const meta = { _type: `[Blocked:${ctorName}]` };
|
|
376
|
+
try {
|
|
377
|
+
if (ctorName?.includes("Socket")) {
|
|
378
|
+
meta.remoteAddress = currentVal.remoteAddress;
|
|
379
|
+
meta.remotePort = currentVal.remotePort;
|
|
380
|
+
meta.localPort = currentVal.localPort;
|
|
381
|
+
}
|
|
382
|
+
else if (ctorName === "Server") {
|
|
383
|
+
meta.listening = currentVal.listening;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
catch { }
|
|
387
|
+
currentVal = meta;
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if ((ctorName === "IncomingMessage" || ctorName === "Request") &&
|
|
392
|
+
!currentVal._type) {
|
|
393
|
+
seen.set(obj, currentPath);
|
|
394
|
+
currentVal = {
|
|
395
|
+
_type: "[XyPriss Request]",
|
|
396
|
+
method: currentVal.method,
|
|
397
|
+
url: currentVal.url,
|
|
398
|
+
headers: SafeSerializer.sanitizeHeaders(currentVal.headers),
|
|
399
|
+
query: currentVal.query,
|
|
400
|
+
params: currentVal.params,
|
|
401
|
+
body: currentVal.body ? "[Request Body]" : undefined,
|
|
402
|
+
ip: currentVal.ip,
|
|
403
|
+
};
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
if ((ctorName === "ServerResponse" || ctorName === "Response") &&
|
|
407
|
+
!currentVal._type) {
|
|
408
|
+
seen.set(obj, currentPath);
|
|
409
|
+
currentVal = {
|
|
410
|
+
_type: "[XyPriss Response]",
|
|
411
|
+
statusCode: currentVal.statusCode,
|
|
412
|
+
statusMessage: currentVal.statusMessage,
|
|
413
|
+
headersSent: currentVal.headersSent,
|
|
414
|
+
};
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
// Heuristic: looks like a duck-typed XyPriss request (Axios config, etc)
|
|
418
|
+
if (currentVal.method &&
|
|
419
|
+
currentVal.url &&
|
|
420
|
+
currentVal.headers &&
|
|
421
|
+
!Array.isArray(currentVal) &&
|
|
422
|
+
!currentVal._type) {
|
|
423
|
+
seen.set(obj, currentPath);
|
|
424
|
+
currentVal = {
|
|
425
|
+
_type: "[XyPriss Request-like]",
|
|
426
|
+
method: currentVal.method,
|
|
427
|
+
url: currentVal.url,
|
|
428
|
+
headers: SafeSerializer.sanitizeHeaders(currentVal.headers),
|
|
429
|
+
};
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
// --- Arrays ---
|
|
433
|
+
if (Array.isArray(currentVal)) {
|
|
434
|
+
seen.set(obj, currentPath);
|
|
435
|
+
const arr = currentVal;
|
|
436
|
+
if (arr.length === 0) {
|
|
437
|
+
chunks.push("[]");
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
const limit = Math.min(arr.length, opts.maxArrayItems);
|
|
441
|
+
const truncatedArray = limit < arr.length;
|
|
442
|
+
chunks.push("[");
|
|
443
|
+
// Push items onto the stack in reverse order so they execute in order
|
|
444
|
+
// We schedule a "close" task last (it runs after all items)
|
|
445
|
+
const closeIdx = chunks.length; // slot reserved below
|
|
446
|
+
chunks.push(""); // placeholder for closing bracket / truncation note
|
|
447
|
+
const itemTasks = [];
|
|
448
|
+
for (let i = 0; i < limit; i++) {
|
|
449
|
+
const idx = i;
|
|
450
|
+
itemTasks.push(() => {
|
|
451
|
+
if (idx > 0)
|
|
452
|
+
chunks.push(",");
|
|
453
|
+
process(arr[idx], `${currentPath}[${idx}]`, currentDepth + 1);
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
// The close task
|
|
457
|
+
const closeTask = () => {
|
|
458
|
+
if (truncatedArray) {
|
|
459
|
+
chunks.push(",");
|
|
460
|
+
chunks.push(`"...[${arr.length - limit} more items truncated]"`);
|
|
461
|
+
}
|
|
462
|
+
chunks[closeIdx] = ""; // clear placeholder
|
|
463
|
+
chunks.push("]");
|
|
464
|
+
};
|
|
465
|
+
// Push close task first, then items in REVERSE order so Task(0) is on top
|
|
466
|
+
stack.push(closeTask);
|
|
467
|
+
for (let i = itemTasks.length - 1; i >= 0; i--) {
|
|
468
|
+
stack.push(itemTasks[i]);
|
|
469
|
+
}
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
// --- Plain objects ---
|
|
473
|
+
seen.set(obj, currentPath);
|
|
474
|
+
const keys = opts.includeNonEnumerable
|
|
475
|
+
? Object.getOwnPropertyNames(obj)
|
|
476
|
+
: Object.keys(obj);
|
|
477
|
+
if (keys.length === 0) {
|
|
478
|
+
chunks.push("{}");
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
const limit = Math.min(keys.length, opts.maxObjectKeys);
|
|
482
|
+
const truncatedObj = limit < keys.length;
|
|
483
|
+
chunks.push("{");
|
|
484
|
+
const closeIdx = chunks.length;
|
|
485
|
+
chunks.push(""); // placeholder
|
|
486
|
+
const keyTasks = [];
|
|
487
|
+
for (let i = 0; i < limit; i++) {
|
|
488
|
+
const k = keys[i];
|
|
489
|
+
const idx = i;
|
|
490
|
+
keyTasks.push(() => {
|
|
491
|
+
if (idx > 0)
|
|
492
|
+
chunks.push(",");
|
|
493
|
+
chunks.push(JSON.stringify(k));
|
|
494
|
+
chunks.push(":");
|
|
495
|
+
let v;
|
|
496
|
+
try {
|
|
497
|
+
v = obj[k];
|
|
498
|
+
}
|
|
499
|
+
catch {
|
|
500
|
+
v = "[Property Access Error]";
|
|
501
|
+
}
|
|
502
|
+
process(v, `${currentPath}.${k}`, currentDepth + 1);
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
const closeTask = () => {
|
|
506
|
+
if (truncatedObj) {
|
|
507
|
+
chunks.push(",");
|
|
508
|
+
chunks.push(`"...[${keys.length - limit} more keys truncated]":null`);
|
|
509
|
+
}
|
|
510
|
+
chunks[closeIdx] = "";
|
|
511
|
+
chunks.push("}");
|
|
512
|
+
};
|
|
513
|
+
stack.push(closeTask);
|
|
514
|
+
for (let i = keyTasks.length - 1; i >= 0; i--) {
|
|
515
|
+
stack.push(keyTasks[i]);
|
|
516
|
+
}
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
};
|
|
520
|
+
// Seed the stack with the root
|
|
521
|
+
stack.push(() => process(value, path, depth));
|
|
522
|
+
// Drain the stack — this is the non-recursive loop
|
|
523
|
+
while (stack.length > 0) {
|
|
524
|
+
const task = stack.pop();
|
|
525
|
+
task();
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
// -------------------------------------------------------------------------
|
|
529
|
+
// UTILITIES
|
|
530
|
+
// -------------------------------------------------------------------------
|
|
531
|
+
/** Merge user options with defaults, always producing a fully-defined object */
|
|
532
|
+
static mergeOptions(options) {
|
|
533
|
+
return { ...this.DEFAULT_OPTIONS, ...options };
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Truncate a string at a safe Unicode boundary (no split surrogates).
|
|
537
|
+
*/
|
|
538
|
+
static safeTruncate(s, maxLen) {
|
|
539
|
+
if (s.length <= maxLen)
|
|
540
|
+
return s;
|
|
541
|
+
// Walk back from maxLen until we find a non-low-surrogate boundary
|
|
542
|
+
let i = maxLen;
|
|
543
|
+
while (i > 0 && s.charCodeAt(i) >= 0xdc00 && s.charCodeAt(i) <= 0xdfff) {
|
|
544
|
+
i--;
|
|
545
|
+
}
|
|
546
|
+
return s.substring(0, i);
|
|
547
|
+
}
|
|
548
|
+
/** Redact sensitive HTTP headers */
|
|
549
|
+
static sanitizeHeaders(headers) {
|
|
550
|
+
if (!headers || typeof headers !== "object")
|
|
551
|
+
return headers;
|
|
552
|
+
const sanitized = {};
|
|
553
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
554
|
+
sanitized[k] = SENSITIVE_HEADERS.has(k.toLowerCase()) ? "[REDACTED]" : v;
|
|
555
|
+
}
|
|
556
|
+
return sanitized;
|
|
557
|
+
}
|
|
558
|
+
/** Duck-type detection for XyPriss req/res objects */
|
|
559
|
+
static isXyPrissObject(arg) {
|
|
560
|
+
if (!arg || typeof arg !== "object")
|
|
561
|
+
return false;
|
|
562
|
+
const a = arg;
|
|
563
|
+
const name = a.constructor?.name;
|
|
564
|
+
if (name === "IncomingMessage" ||
|
|
565
|
+
name === "ServerResponse" ||
|
|
566
|
+
name === "Request" ||
|
|
567
|
+
name === "Response")
|
|
568
|
+
return true;
|
|
569
|
+
if (a.method && a.url && a.headers)
|
|
570
|
+
return true;
|
|
571
|
+
if (a.statusCode !== undefined && a.headersSent !== undefined)
|
|
572
|
+
return true;
|
|
573
|
+
return false;
|
|
574
|
+
}
|
|
321
575
|
}
|
|
322
576
|
exports.SafeSerializer = SafeSerializer;
|
|
323
|
-
SafeSerializer.DEFAULT_OPTIONS = {
|
|
324
|
-
maxDepth: 10,
|
|
325
|
-
maxLength: 10000,
|
|
326
|
-
includeNonEnumerable: false,
|
|
327
|
-
truncateStrings: 1000,
|
|
328
|
-
fastMode: false,
|
|
329
|
-
};
|
|
330
577
|
//# sourceMappingURL=safe-serializer.js.map
|