@zintrust/core 2.5.7 → 2.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zintrust/core",
3
- "version": "2.5.7",
3
+ "version": "2.5.9",
4
4
  "description": "Production-grade TypeScript backend framework for JavaScript",
5
5
  "homepage": "https://zintrust.com",
6
6
  "repository": {
@@ -1 +1 @@
1
- {"version":3,"file":"BodyParsingMiddleware.d.ts","sourceRoot":"","sources":["../../../../src/http/middleware/BodyParsingMiddleware.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAOH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAiT9D;;;GAGG;AACH,eAAO,MAAM,qBAAqB,EAAE,UA4CnC,CAAC;AAEF,eAAe,qBAAqB,CAAC"}
1
+ {"version":3,"file":"BodyParsingMiddleware.d.ts","sourceRoot":"","sources":["../../../../src/http/middleware/BodyParsingMiddleware.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAOH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAkY9D;;;GAGG;AACH,eAAO,MAAM,qBAAqB,EAAE,UA4CnC,CAAC;AAEF,eAAe,qBAAqB,CAAC"}
@@ -156,6 +156,29 @@ const parseJsonBody = (text, contentType, res) => {
156
156
  return null;
157
157
  }
158
158
  };
159
+ /**
160
+ * Attempt to recover a JSON string that was stored as raw text or bytes
161
+ * due to a missing or incorrect Content-Type header.
162
+ *
163
+ * Only plain objects `{...}` are considered for recovery — arrays, strings,
164
+ * and primitives are left unchanged. This minimizes false positives for
165
+ * legitimate text or binary payloads that happen to start with `{`.
166
+ */
167
+ const tryRecoverTextJsonBody = (body) => {
168
+ const trimmed = body.trim();
169
+ if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
170
+ try {
171
+ const parsed = JSON.parse(trimmed);
172
+ if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
173
+ return parsed;
174
+ }
175
+ }
176
+ catch {
177
+ // Not valid JSON — leave original unchanged
178
+ }
179
+ }
180
+ return body;
181
+ };
159
182
  const setRequestBody = (req, rawResult, contentType) => {
160
183
  const isUrlEncoded = contentType.includes('application/x-www-form-urlencoded');
161
184
  const isText = contentType.startsWith('text/') || contentType.includes('application/xml');
@@ -164,9 +187,26 @@ const setRequestBody = (req, rawResult, contentType) => {
164
187
  req.setBody(parseUrlEncodedBody(text));
165
188
  }
166
189
  else if (isText) {
167
- req.setBody(text);
190
+ // Recovery: JSON body sent with text Content-Type (e.g. text/plain)
191
+ // is stored as a raw string. Detect a JSON object and parse it.
192
+ req.setBody(tryRecoverTextJsonBody(text));
168
193
  }
169
194
  else if (contentType !== '') {
195
+ // Recovery: JSON body sent with an unknown Content-Type is stored as
196
+ // raw bytes. Try to decode as text and check for JSON.
197
+ const decoded = text.trim();
198
+ if (decoded.startsWith('{') && decoded.endsWith('}')) {
199
+ try {
200
+ const parsed = JSON.parse(decoded);
201
+ if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
202
+ req.setBody(parsed);
203
+ return;
204
+ }
205
+ }
206
+ catch {
207
+ // Not valid JSON — fall through to bytes
208
+ }
209
+ }
170
210
  req.setBody(rawResult.bytes);
171
211
  }
172
212
  };
@@ -198,39 +238,79 @@ const reuseExistingRawBody = (req, res, contentType, existingBytes, existingText
198
238
  }
199
239
  return applyParsedRequestBody(req, res, rawResult, contentType);
200
240
  };
201
- const parseUrlEncodedBody = (text) => {
241
+ /**
242
+ * Attempt to parse a string as a JSON object.
243
+ * Returns the parsed object on success, or null if the string is not
244
+ * a valid JSON object literal (plain object `{...}`, not array or primitive).
245
+ */
246
+ const tryParseJsonObject = (text) => {
247
+ const trimmed = text.trim();
248
+ if (!trimmed.startsWith('{') || !trimmed.endsWith('}'))
249
+ return null;
250
+ try {
251
+ const parsed = JSON.parse(trimmed);
252
+ if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
253
+ return parsed;
254
+ }
255
+ }
256
+ catch {
257
+ // Not valid JSON
258
+ }
259
+ return null;
260
+ };
261
+ /**
262
+ * Parse URL-encoded form data into a key-value record.
263
+ * Multiple values for the same key are collected into an array.
264
+ */
265
+ const parseUrlEncodedParams = (text) => {
202
266
  const out = {};
203
267
  const params = new URLSearchParams(text);
204
268
  for (const [key, value] of params.entries()) {
205
269
  const existing = out[key];
206
270
  if (existing === undefined) {
207
271
  out[key] = value;
208
- continue;
209
272
  }
210
- if (Array.isArray(existing)) {
273
+ else if (Array.isArray(existing)) {
211
274
  existing.push(value);
212
- continue;
213
275
  }
214
- out[key] = [existing, value];
276
+ else {
277
+ out[key] = [existing, value];
278
+ }
279
+ }
280
+ return out;
281
+ };
282
+ /**
283
+ * Detect and recover from a degenerate case where a JSON payload was sent
284
+ * with a url-encoded Content-Type header. URLSearchParams treats the entire
285
+ * JSON string as a single key with an empty value, so we try parsing that key.
286
+ */
287
+ const tryRecoverDegenerateJson = (out) => {
288
+ const keys = Object.keys(out);
289
+ if (keys.length !== 1 || out[keys[0]] !== '')
290
+ return null;
291
+ const maybeJson = keys[0].trim();
292
+ const parsed = tryParseJsonObject(maybeJson);
293
+ if (parsed !== null) {
294
+ return parsed;
215
295
  }
296
+ return null;
297
+ };
298
+ const parseUrlEncodedBody = (text) => {
299
+ // JSON-first recovery: parse raw text before URLSearchParams touches it.
300
+ // URLSearchParams splits on the first '=' (corrupting base64-padded values)
301
+ // and converts '+' to spaces, making encrypted/base64 fields unrecoverable.
302
+ const jsonResult = tryParseJsonObject(text);
303
+ if (jsonResult !== null) {
304
+ return jsonResult;
305
+ }
306
+ const out = parseUrlEncodedParams(text);
216
307
  // Recovery: if a JSON payload is sent with the wrong Content-Type header
217
308
  // (e.g. application/x-www-form-urlencoded instead of application/json),
218
309
  // URLSearchParams will treat the entire JSON string as a single key with
219
310
  // an empty value. Detect this degenerate case and fall back to JSON.parse.
220
- const keys = Object.keys(out);
221
- if (keys.length === 1 && out[keys[0]] === '') {
222
- const maybeJson = keys[0].trim();
223
- if (maybeJson.startsWith('{') && maybeJson.endsWith('}')) {
224
- try {
225
- const parsed = JSON.parse(maybeJson);
226
- if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
227
- return parsed;
228
- }
229
- }
230
- catch {
231
- // Not valid JSON — leave original result unchanged
232
- }
233
- }
311
+ const recovered = tryRecoverDegenerateJson(out);
312
+ if (recovered !== null) {
313
+ return recovered;
234
314
  }
235
315
  return out;
236
316
  };
@@ -1 +1 @@
1
- {"version":3,"file":"BodyParsers.d.ts","sourceRoot":"","sources":["../../../../src/http/parsers/BodyParsers.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,UAAU,gBAAgB;IACxB,EAAE,EAAE,OAAO,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,GAAG,OAAO,EAAE,CAAC;IACpD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,UAAU,WAAW;IACnB,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC;IACvC,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,gBAAgB,CAAC;CAChD;AAiLD,eAAO,MAAM,WAAW;;;;IAKtB;;OAEG;cACO,WAAW,EAAE;IAIvB;;OAEG;4BACqB,MAAM,GAAG,WAAW,GAAG,SAAS;IAIxD;;OAEG;uBACgB,MAAM,QAAQ,MAAM,GAAG,MAAM,GAAG,gBAAgB;EAQnE,CAAC;AAEH,eAAe,WAAW,CAAC"}
1
+ {"version":3,"file":"BodyParsers.d.ts","sourceRoot":"","sources":["../../../../src/http/parsers/BodyParsers.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,UAAU,gBAAgB;IACxB,EAAE,EAAE,OAAO,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,GAAG,OAAO,EAAE,CAAC;IACpD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,UAAU,WAAW;IACnB,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC;IACvC,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,gBAAgB,CAAC;CAChD;AAoND,eAAO,MAAM,WAAW;;;;IAKtB;;OAEG;cACO,WAAW,EAAE;IAIvB;;OAEG;4BACqB,MAAM,GAAG,WAAW,GAAG,SAAS;IAIxD;;OAEG;uBACgB,MAAM,QAAQ,MAAM,GAAG,MAAM,GAAG,gBAAgB;EAQnE,CAAC;AAEH,eAAe,WAAW,CAAC"}
@@ -27,6 +27,48 @@ const tryRecoverJsonBody = (result) => {
27
27
  }
28
28
  return undefined;
29
29
  };
30
+ /**
31
+ * Try to parse raw text as JSON before URLSearchParams touches it.
32
+ * This handles cases where JSON is sent with application/x-www-form-urlencoded Content-Type,
33
+ * which would be corrupted by URLSearchParams (splits on '=', converts '+' to spaces).
34
+ */
35
+ const tryParseJsonRaw = (text) => {
36
+ const trimmed = text.trim();
37
+ if (!trimmed.startsWith('{') || !trimmed.endsWith('}'))
38
+ return undefined;
39
+ try {
40
+ const parsed = JSON.parse(trimmed);
41
+ if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
42
+ return parsed;
43
+ }
44
+ }
45
+ catch {
46
+ // Not valid JSON
47
+ }
48
+ return undefined;
49
+ };
50
+ /**
51
+ * Parse URL-encoded string into key-value pairs, handling duplicate keys as arrays.
52
+ */
53
+ const parseUrlEncodedParams = (text) => {
54
+ const params = new URLSearchParams(text);
55
+ const result = {};
56
+ for (const [key, value] of params.entries()) {
57
+ if (Object.prototype.hasOwnProperty.call(result, key)) {
58
+ const existing = result[key];
59
+ if (Array.isArray(existing)) {
60
+ existing.push(value);
61
+ }
62
+ else {
63
+ result[key] = [existing, value];
64
+ }
65
+ }
66
+ else {
67
+ result[key] = value;
68
+ }
69
+ }
70
+ return result;
71
+ };
30
72
  /**
31
73
  * URL-encoded form data parser
32
74
  * Content-Type: application/x-www-form-urlencoded
@@ -38,25 +80,14 @@ const FormDataParser = {
38
80
  const text = typeof body === 'string' ? body : body.toString('utf-8');
39
81
  if (!text.trim())
40
82
  return { ok: true, data: {} };
41
- const params = new URLSearchParams(text);
42
- const result = {};
43
- for (const [key, value] of params.entries()) {
44
- if (Object.prototype.hasOwnProperty.call(result, key)) {
45
- // Handle multiple values with same key
46
- const existing = result[key];
47
- if (Array.isArray(existing)) {
48
- existing.push(value);
49
- }
50
- else {
51
- result[key] = [existing, value];
52
- }
53
- }
54
- else {
55
- result[key] = value;
56
- }
83
+ // JSON-first recovery: parse raw text before URLSearchParams touches it.
84
+ const rawJsonResult = tryParseJsonRaw(text);
85
+ if (rawJsonResult !== undefined) {
86
+ return { ok: true, data: rawJsonResult };
57
87
  }
58
- // Recovery: if a JSON payload is sent with the wrong Content-Type header,
59
- // URLSearchParams will treat the entire JSON string as a single key with
88
+ const result = parseUrlEncodedParams(text);
89
+ // Recovery: if a JSON payload was sent with wrong Content-Type,
90
+ // URLSearchParams treats the whole JSON string as a single key with
60
91
  // an empty value. Detect this degenerate case and fall back to JSON.parse.
61
92
  const recovered = tryRecoverJsonBody(result);
62
93
  if (recovered !== undefined) {
package/src/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  /**
2
- * @zintrust/core v2.5.7
2
+ * @zintrust/core v2.5.9
3
3
  *
4
4
  * ZinTrust Framework - Production-Grade TypeScript Backend
5
5
  * Built for performance, type safety, and exceptional developer experience
6
6
  *
7
7
  * Build Information:
8
- * Built: 2026-06-12T18:33:47.619Z
8
+ * Built: 2026-06-13T09:17:52.095Z
9
9
  * Node: >=20.0.0
10
10
  * License: MIT
11
11
  *
@@ -21,7 +21,7 @@
21
21
  * Available at runtime for debugging and health checks
22
22
  */
23
23
  export const ZINTRUST_VERSION = '0.1.41';
24
- export const ZINTRUST_BUILD_DATE = '2026-06-12T18:33:47.574Z'; // Replaced during build
24
+ export const ZINTRUST_BUILD_DATE = '2026-06-13T09:17:51.999Z'; // Replaced during build
25
25
  export { Application } from './boot/Application.js';
26
26
  export { AwsSigV4 } from './common/index.js';
27
27
  export { SignedRequest } from './security/SignedRequest.js';