@zero-server/sdk 0.9.1 → 0.9.3

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 (128) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +460 -443
  3. package/index.js +414 -412
  4. package/lib/app.js +1172 -1172
  5. package/lib/auth/authorize.js +399 -399
  6. package/lib/auth/enrollment.js +367 -367
  7. package/lib/auth/index.js +57 -57
  8. package/lib/auth/jwt.js +731 -731
  9. package/lib/auth/oauth.js +362 -362
  10. package/lib/auth/session.js +588 -588
  11. package/lib/auth/trustedDevice.js +409 -409
  12. package/lib/auth/twoFactor.js +1150 -1150
  13. package/lib/auth/webauthn.js +946 -946
  14. package/lib/body/index.js +14 -14
  15. package/lib/body/json.js +109 -109
  16. package/lib/body/multipart.js +440 -440
  17. package/lib/body/raw.js +71 -71
  18. package/lib/body/rawBuffer.js +160 -160
  19. package/lib/body/sendError.js +25 -25
  20. package/lib/body/text.js +75 -75
  21. package/lib/body/typeMatch.js +41 -41
  22. package/lib/body/urlencoded.js +235 -235
  23. package/lib/cli.js +845 -845
  24. package/lib/cluster.js +666 -666
  25. package/lib/debug.js +372 -372
  26. package/lib/env/index.js +465 -465
  27. package/lib/errors.js +683 -683
  28. package/lib/fetch/index.js +256 -256
  29. package/lib/grpc/balancer.js +378 -378
  30. package/lib/grpc/call.js +708 -708
  31. package/lib/grpc/client.js +764 -764
  32. package/lib/grpc/codec.js +1221 -1221
  33. package/lib/grpc/credentials.js +398 -398
  34. package/lib/grpc/frame.js +262 -262
  35. package/lib/grpc/health.js +287 -287
  36. package/lib/grpc/index.js +121 -121
  37. package/lib/grpc/metadata.js +461 -461
  38. package/lib/grpc/proto.js +821 -821
  39. package/lib/grpc/reflection.js +590 -590
  40. package/lib/grpc/server.js +445 -445
  41. package/lib/grpc/status.js +118 -118
  42. package/lib/grpc/watch.js +173 -173
  43. package/lib/http/index.js +10 -10
  44. package/lib/http/request.js +727 -727
  45. package/lib/http/response.js +799 -799
  46. package/lib/lifecycle.js +557 -557
  47. package/lib/middleware/compress.js +230 -230
  48. package/lib/middleware/cookieParser.js +237 -237
  49. package/lib/middleware/cors.js +93 -93
  50. package/lib/middleware/csrf.js +137 -137
  51. package/lib/middleware/errorHandler.js +101 -101
  52. package/lib/middleware/helmet.js +175 -175
  53. package/lib/middleware/index.js +19 -17
  54. package/lib/middleware/logger.js +74 -74
  55. package/lib/middleware/rateLimit.js +88 -88
  56. package/lib/middleware/requestId.js +53 -53
  57. package/lib/middleware/static.js +326 -326
  58. package/lib/middleware/timeout.js +71 -71
  59. package/lib/middleware/validator.js +255 -255
  60. package/lib/observe/health.js +326 -326
  61. package/lib/observe/index.js +50 -50
  62. package/lib/observe/logger.js +359 -359
  63. package/lib/observe/metrics.js +805 -805
  64. package/lib/observe/tracing.js +592 -592
  65. package/lib/orm/adapters/json.js +290 -290
  66. package/lib/orm/adapters/memory.js +764 -764
  67. package/lib/orm/adapters/mongo.js +764 -764
  68. package/lib/orm/adapters/mysql.js +933 -933
  69. package/lib/orm/adapters/postgres.js +1144 -1144
  70. package/lib/orm/adapters/redis.js +1534 -1534
  71. package/lib/orm/adapters/sql-base.js +212 -212
  72. package/lib/orm/adapters/sqlite.js +858 -858
  73. package/lib/orm/audit.js +649 -649
  74. package/lib/orm/cache.js +394 -394
  75. package/lib/orm/geo.js +387 -387
  76. package/lib/orm/index.js +784 -784
  77. package/lib/orm/migrate.js +432 -432
  78. package/lib/orm/model.js +1706 -1706
  79. package/lib/orm/plugin.js +375 -375
  80. package/lib/orm/procedures.js +836 -836
  81. package/lib/orm/profiler.js +233 -233
  82. package/lib/orm/query.js +1772 -1772
  83. package/lib/orm/replicas.js +241 -241
  84. package/lib/orm/schema.js +307 -307
  85. package/lib/orm/search.js +380 -380
  86. package/lib/orm/seed/data/commerce.js +136 -136
  87. package/lib/orm/seed/data/internet.js +111 -111
  88. package/lib/orm/seed/data/locations.js +204 -204
  89. package/lib/orm/seed/data/names.js +338 -338
  90. package/lib/orm/seed/data/person.js +128 -128
  91. package/lib/orm/seed/data/phone.js +211 -211
  92. package/lib/orm/seed/data/words.js +134 -134
  93. package/lib/orm/seed/factory.js +178 -178
  94. package/lib/orm/seed/fake.js +1186 -1186
  95. package/lib/orm/seed/index.js +18 -18
  96. package/lib/orm/seed/rng.js +70 -70
  97. package/lib/orm/seed/seeder.js +124 -124
  98. package/lib/orm/seed/unique.js +68 -68
  99. package/lib/orm/snapshot.js +366 -366
  100. package/lib/orm/tenancy.js +605 -605
  101. package/lib/orm/views.js +350 -350
  102. package/lib/router/index.js +436 -436
  103. package/lib/sse/index.js +8 -8
  104. package/lib/sse/stream.js +349 -349
  105. package/lib/ws/connection.js +451 -451
  106. package/lib/ws/handshake.js +125 -125
  107. package/lib/ws/index.js +14 -14
  108. package/lib/ws/room.js +223 -223
  109. package/package.json +73 -73
  110. package/types/app.d.ts +223 -223
  111. package/types/auth.d.ts +520 -520
  112. package/types/body.d.ts +14 -0
  113. package/types/cli.d.ts +2 -0
  114. package/types/cluster.d.ts +75 -75
  115. package/types/env.d.ts +80 -80
  116. package/types/errors.d.ts +316 -316
  117. package/types/fetch.d.ts +43 -43
  118. package/types/grpc.d.ts +432 -432
  119. package/types/index.d.ts +384 -384
  120. package/types/lifecycle.d.ts +60 -60
  121. package/types/middleware.d.ts +320 -320
  122. package/types/observe.d.ts +304 -304
  123. package/types/orm.d.ts +1887 -1887
  124. package/types/request.d.ts +109 -109
  125. package/types/response.d.ts +157 -157
  126. package/types/router.d.ts +78 -78
  127. package/types/sse.d.ts +78 -78
  128. package/types/websocket.d.ts +126 -126
@@ -1,42 +1,42 @@
1
- /**
2
- * @module body/typeMatch
3
- * @private
4
- * @description Shared Content-Type matching utility for body parsers.
5
- */
6
-
7
- /**
8
- * Check whether a Content-Type header matches the configured type filter.
9
- *
10
- * @private
11
- * @param {string} contentType - The request Content-Type header value.
12
- * @param {string|string[]|function} typeOpt - MIME pattern to match against (e.g. 'application/json', 'text/*', '*\/*'),
13
- * an array of patterns, or a custom predicate `(ct) => boolean`.
1
+ /**
2
+ * @module body/typeMatch
3
+ * @private
4
+ * @description Shared Content-Type matching utility for body parsers.
5
+ */
6
+
7
+ /**
8
+ * Check whether a Content-Type header matches the configured type filter.
9
+ *
10
+ * @private
11
+ * @param {string} contentType - The request Content-Type header value.
12
+ * @param {string|string[]|function} typeOpt - MIME pattern to match against (e.g. 'application/json', 'text/*', '*\/*'),
13
+ * an array of patterns, or a custom predicate `(ct) => boolean`.
14
14
  * @returns {boolean} Boolean result.
15
- */
16
- function isTypeMatch(contentType, typeOpt)
17
- {
18
- if (!typeOpt) return true;
19
- if (typeof typeOpt === 'function') return !!typeOpt(contentType);
20
- if (Array.isArray(typeOpt)) return typeOpt.some(t => isTypeMatch(contentType, t));
21
- if (!contentType) return false;
22
- if (typeOpt === '*/*') return true;
23
- // Strip charset/parameters from content-type for proper matching
24
- const semiIdx = contentType.indexOf(';');
25
- const baseType = semiIdx !== -1 ? contentType.substring(0, semiIdx).trim() : contentType;
26
- if (typeOpt.endsWith('/*'))
27
- {
28
- return baseType.startsWith(typeOpt.slice(0, -1));
29
- }
30
- // Suffix pattern: application/*+json matches application/vnd.api+json
31
- const starIdx = typeOpt.indexOf('/*+');
32
- if (starIdx !== -1)
33
- {
34
- const prefix = typeOpt.slice(0, starIdx + 1); // 'application/'
35
- const suffix = typeOpt.slice(starIdx + 2); // '+json'
36
- return baseType.startsWith(prefix) && baseType.endsWith(suffix);
37
- }
38
- // Exact or substring match against the base type only
39
- return baseType.indexOf(typeOpt) !== -1;
40
- }
41
-
42
- module.exports = isTypeMatch;
15
+ */
16
+ function isTypeMatch(contentType, typeOpt)
17
+ {
18
+ if (!typeOpt) return true;
19
+ if (typeof typeOpt === 'function') return !!typeOpt(contentType);
20
+ if (Array.isArray(typeOpt)) return typeOpt.some(t => isTypeMatch(contentType, t));
21
+ if (!contentType) return false;
22
+ if (typeOpt === '*/*') return true;
23
+ // Strip charset/parameters from content-type for proper matching
24
+ const semiIdx = contentType.indexOf(';');
25
+ const baseType = semiIdx !== -1 ? contentType.substring(0, semiIdx).trim() : contentType;
26
+ if (typeOpt.endsWith('/*'))
27
+ {
28
+ return baseType.startsWith(typeOpt.slice(0, -1));
29
+ }
30
+ // Suffix pattern: application/*+json matches application/vnd.api+json
31
+ const starIdx = typeOpt.indexOf('/*+');
32
+ if (starIdx !== -1)
33
+ {
34
+ const prefix = typeOpt.slice(0, starIdx + 1); // 'application/'
35
+ const suffix = typeOpt.slice(starIdx + 2); // '+json'
36
+ return baseType.startsWith(prefix) && baseType.endsWith(suffix);
37
+ }
38
+ // Exact or substring match against the base type only
39
+ return baseType.indexOf(typeOpt) !== -1;
40
+ }
41
+
42
+ module.exports = isTypeMatch;
@@ -1,235 +1,235 @@
1
- /**
2
- * @module body/urlencoded
3
- * @description URL-encoded body-parsing middleware.
4
- * Supports both flat (`URLSearchParams`) and extended
5
- * (nested bracket syntax) parsing modes.
6
- * Stores the raw buffer on `req.rawBody` for signature verification.
7
- */
8
- const rawBuffer = require('./rawBuffer');
9
- const isTypeMatch = require('./typeMatch');
10
- const sendError = require('./sendError');
11
-
12
- /**
13
- * Append a value to an existing key, converting to an array when needed.
14
- *
15
- * @private
16
- * @param {*} prev - Previous value for the key (or `undefined`).
17
- * @param {string} val - New value to append.
18
- * @returns {string|string[]} Merged value.
19
- */
20
- function appendValue(prev, val)
21
- {
22
- if (prev === undefined) return val;
23
- if (Array.isArray(prev)) { prev.push(val); return prev; }
24
- // convert existing scalar or object into array to hold multiple values
25
- return [prev, val];
26
- }
27
-
28
- /**
29
- * Create a URL-encoded body-parsing middleware.
30
- *
31
- * @param {object} [options] - Configuration options.
32
- * @param {string|number} [options.limit] - Max body size (e.g. `'10kb'`). Default `'1mb'`.
33
- * @param {string|string[]|Function} [options.type='application/x-www-form-urlencoded'] - Content-Type(s) to match.
34
- * @param {boolean} [options.extended=false] - Use nested bracket parsing (e.g. `a[b][c]=1`).
35
- * @param {boolean} [options.requireSecure=false] - When true, reject non-HTTPS requests with 403.
36
- * @param {number} [options.parameterLimit=1000] - Max number of parameters. Prevents DoS via huge payloads.
37
- * @param {number} [options.depth=32] - Max nesting depth for bracket syntax. Prevents deep-nesting DoS.
38
- * @param {Function} [options.verify] - `verify(req, res, buf, encoding)` — called before parsing. Throw to reject with 403.
39
- * @param {boolean} [options.inflate=true] - Decompress gzip/deflate/br bodies.
40
- * @returns {Function} Async middleware `(req, res, next) => void`.
41
- *
42
- * @example
43
- * const { urlencoded } = require('@zero-server/sdk');
44
- *
45
- * // Flat parsing (default)
46
- * app.use(urlencoded({ limit: '100kb' }));
47
- *
48
- * // Nested bracket syntax
49
- * app.use(urlencoded({ extended: true }));
50
- *
51
- * app.post('/form', (req, res) => {
52
- * console.log(req.body); // { name: 'Tony', age: '30' }
53
- * res.json(req.body);
54
- * });
55
- */
56
- function urlencoded(options = {})
57
- {
58
- const opts = options || {};
59
- const limit = opts.limit !== undefined ? opts.limit : '1mb';
60
- const typeOpt = opts.type || 'application/x-www-form-urlencoded';
61
- const extended = !!opts.extended;
62
- const requireSecure = !!opts.requireSecure;
63
- const parameterLimit = opts.parameterLimit !== undefined ? opts.parameterLimit : 1000;
64
- const maxDepth = opts.depth !== undefined ? opts.depth : 32;
65
- const verify = opts.verify;
66
- const inflate = opts.inflate !== undefined ? opts.inflate : true;
67
-
68
- return async (req, res, next) =>
69
- {
70
- if (requireSecure && !req.secure) return sendError(res, 403, 'HTTPS required');
71
- const ct = (req.headers['content-type'] || '');
72
- if (!isTypeMatch(ct, typeOpt)) return next();
73
- try
74
- {
75
- const buf = await rawBuffer(req, { limit, inflate });
76
-
77
- // Store raw body for signature verification
78
- req.rawBody = buf;
79
-
80
- // Optional verification callback
81
- if (verify)
82
- {
83
- try { verify(req, res, buf, 'utf8'); }
84
- catch (e) { return sendError(res, 403, e.message || 'verification failed'); }
85
- }
86
-
87
- const txt = buf.toString('utf8');
88
- if (!extended)
89
- {
90
- const params = new URLSearchParams(txt);
91
- // Enforce parameter limit
92
- if (parameterLimit)
93
- {
94
- let count = 0;
95
- for (const _ of params) { if (++count > parameterLimit) return sendError(res, 413, 'too many parameters'); }
96
- }
97
- req.body = Object.fromEntries(params);
98
- }
99
- else
100
- {
101
- // extended parsing: support nested bracket syntax like a[b][c]=1 and arrays a[]=1
102
- const out = {};
103
- if (txt.trim() === '') { req.body = out; return next(); }
104
- const pairs = txt.split('&');
105
- // Enforce parameter limit
106
- if (parameterLimit && pairs.length > parameterLimit)
107
- {
108
- return sendError(res, 413, 'too many parameters');
109
- }
110
- for (const p of pairs)
111
- {
112
- if (!p) continue;
113
- const eq = p.indexOf('=');
114
- let k, v;
115
- if (eq === -1) { k = decodeURIComponent(p.replace(/\+/g, ' ')); v = ''; }
116
- else { k = decodeURIComponent(p.slice(0, eq).replace(/\+/g, ' ')); v = decodeURIComponent(p.slice(eq + 1).replace(/\+/g, ' ')); }
117
- // parse key into parts
118
- const parts = [];
119
- const re = /([^\[\]]+)|\[(.*?)\]/g;
120
- let m;
121
- while ((m = re.exec(k)) !== null)
122
- {
123
- const part = m[1] || m[2];
124
- // Prevent prototype pollution
125
- if (part === '__proto__' || part === 'constructor' || part === 'prototype') continue;
126
- parts.push(part);
127
- }
128
-
129
- // Enforce depth limit
130
- if (maxDepth && parts.length > maxDepth)
131
- {
132
- return sendError(res, 400, 'nesting depth exceeded');
133
- }
134
-
135
- // set value into out following parts
136
- // _parent/_parentKey track the container holding `cur` so we can
137
- // convert it from object → array when the `[]` push syntax is used.
138
- let cur = out;
139
- let _parent = null;
140
- let _parentKey = null;
141
- for (let i = 0; i < parts.length; i++)
142
- {
143
- const part = parts[i];
144
- const isLast = (i === parts.length - 1);
145
-
146
- if (part === '')
147
- {
148
- // Empty-bracket array-push syntax: a[]=val / a[][key]=val
149
- if (isLast)
150
- {
151
- // Ensure cur is an array before pushing, converting parent ref if needed
152
- if (!Array.isArray(cur))
153
- {
154
- const arr = [];
155
- if (_parent !== null) _parent[_parentKey] = arr;
156
- cur = arr;
157
- }
158
- cur.push(v);
159
- break;
160
- }
161
- // Intermediate empty bracket — navigate into next element of the array
162
- if (!Array.isArray(cur))
163
- {
164
- const arr = [];
165
- if (_parent !== null) _parent[_parentKey] = arr;
166
- cur = arr;
167
- }
168
- if (cur.length === 0 || typeof cur[cur.length - 1] !== 'object') cur.push({});
169
- _parent = cur;
170
- _parentKey = cur.length - 1;
171
- cur = cur[cur.length - 1];
172
- continue;
173
- }
174
-
175
- // normal key
176
- if (isLast)
177
- {
178
- if (Array.isArray(cur))
179
- {
180
- // numeric key may indicate index
181
- const idx = Number(part);
182
- if (!Number.isNaN(idx)) cur[idx] = appendValue(cur[idx], v);
183
- else cur[part] = appendValue(cur[part], v);
184
- }
185
- else
186
- {
187
- cur[part] = appendValue(cur[part], v);
188
- }
189
- }
190
- else
191
- {
192
- if (Array.isArray(cur))
193
- {
194
- const idx = Number(part);
195
- if (!Number.isNaN(idx))
196
- {
197
- if (!cur[idx]) cur[idx] = {};
198
- _parent = cur;
199
- _parentKey = idx;
200
- cur = cur[idx];
201
- } else
202
- {
203
- // Non-numeric key on array — navigate into last pushed object
204
- if (cur.length === 0) cur.push({});
205
- if (typeof cur[cur.length - 1] !== 'object') cur.push({});
206
- const obj = cur[cur.length - 1];
207
- if (!obj[part]) obj[part] = {};
208
- _parent = obj;
209
- _parentKey = part;
210
- cur = obj[part];
211
- }
212
- }
213
- else
214
- {
215
- if (!cur[part]) cur[part] = {};
216
- _parent = cur;
217
- _parentKey = part;
218
- cur = cur[part];
219
- }
220
- }
221
- }
222
- }
223
- req.body = out;
224
- }
225
- } catch (err)
226
- {
227
- if (err && err.status === 413) return sendError(res, 413, 'payload too large');
228
- if (err && err.status === 415) return sendError(res, 415, err.message || 'unsupported encoding');
229
- req.body = {};
230
- }
231
- next();
232
- };
233
- }
234
-
235
- module.exports = urlencoded;
1
+ /**
2
+ * @module body/urlencoded
3
+ * @description URL-encoded body-parsing middleware.
4
+ * Supports both flat (`URLSearchParams`) and extended
5
+ * (nested bracket syntax) parsing modes.
6
+ * Stores the raw buffer on `req.rawBody` for signature verification.
7
+ */
8
+ const rawBuffer = require('./rawBuffer');
9
+ const isTypeMatch = require('./typeMatch');
10
+ const sendError = require('./sendError');
11
+
12
+ /**
13
+ * Append a value to an existing key, converting to an array when needed.
14
+ *
15
+ * @private
16
+ * @param {*} prev - Previous value for the key (or `undefined`).
17
+ * @param {string} val - New value to append.
18
+ * @returns {string|string[]} Merged value.
19
+ */
20
+ function appendValue(prev, val)
21
+ {
22
+ if (prev === undefined) return val;
23
+ if (Array.isArray(prev)) { prev.push(val); return prev; }
24
+ // convert existing scalar or object into array to hold multiple values
25
+ return [prev, val];
26
+ }
27
+
28
+ /**
29
+ * Create a URL-encoded body-parsing middleware.
30
+ *
31
+ * @param {object} [options] - Configuration options.
32
+ * @param {string|number} [options.limit] - Max body size (e.g. `'10kb'`). Default `'1mb'`.
33
+ * @param {string|string[]|Function} [options.type='application/x-www-form-urlencoded'] - Content-Type(s) to match.
34
+ * @param {boolean} [options.extended=false] - Use nested bracket parsing (e.g. `a[b][c]=1`).
35
+ * @param {boolean} [options.requireSecure=false] - When true, reject non-HTTPS requests with 403.
36
+ * @param {number} [options.parameterLimit=1000] - Max number of parameters. Prevents DoS via huge payloads.
37
+ * @param {number} [options.depth=32] - Max nesting depth for bracket syntax. Prevents deep-nesting DoS.
38
+ * @param {Function} [options.verify] - `verify(req, res, buf, encoding)` — called before parsing. Throw to reject with 403.
39
+ * @param {boolean} [options.inflate=true] - Decompress gzip/deflate/br bodies.
40
+ * @returns {Function} Async middleware `(req, res, next) => void`.
41
+ *
42
+ * @example
43
+ * const { urlencoded } = require('@zero-server/sdk');
44
+ *
45
+ * // Flat parsing (default)
46
+ * app.use(urlencoded({ limit: '100kb' }));
47
+ *
48
+ * // Nested bracket syntax
49
+ * app.use(urlencoded({ extended: true }));
50
+ *
51
+ * app.post('/form', (req, res) => {
52
+ * console.log(req.body); // { name: 'Tony', age: '30' }
53
+ * res.json(req.body);
54
+ * });
55
+ */
56
+ function urlencoded(options = {})
57
+ {
58
+ const opts = options || {};
59
+ const limit = opts.limit !== undefined ? opts.limit : '1mb';
60
+ const typeOpt = opts.type || 'application/x-www-form-urlencoded';
61
+ const extended = !!opts.extended;
62
+ const requireSecure = !!opts.requireSecure;
63
+ const parameterLimit = opts.parameterLimit !== undefined ? opts.parameterLimit : 1000;
64
+ const maxDepth = opts.depth !== undefined ? opts.depth : 32;
65
+ const verify = opts.verify;
66
+ const inflate = opts.inflate !== undefined ? opts.inflate : true;
67
+
68
+ return async (req, res, next) =>
69
+ {
70
+ if (requireSecure && !req.secure) return sendError(res, 403, 'HTTPS required');
71
+ const ct = (req.headers['content-type'] || '');
72
+ if (!isTypeMatch(ct, typeOpt)) return next();
73
+ try
74
+ {
75
+ const buf = await rawBuffer(req, { limit, inflate });
76
+
77
+ // Store raw body for signature verification
78
+ req.rawBody = buf;
79
+
80
+ // Optional verification callback
81
+ if (verify)
82
+ {
83
+ try { verify(req, res, buf, 'utf8'); }
84
+ catch (e) { return sendError(res, 403, e.message || 'verification failed'); }
85
+ }
86
+
87
+ const txt = buf.toString('utf8');
88
+ if (!extended)
89
+ {
90
+ const params = new URLSearchParams(txt);
91
+ // Enforce parameter limit
92
+ if (parameterLimit)
93
+ {
94
+ let count = 0;
95
+ for (const _ of params) { if (++count > parameterLimit) return sendError(res, 413, 'too many parameters'); }
96
+ }
97
+ req.body = Object.fromEntries(params);
98
+ }
99
+ else
100
+ {
101
+ // extended parsing: support nested bracket syntax like a[b][c]=1 and arrays a[]=1
102
+ const out = {};
103
+ if (txt.trim() === '') { req.body = out; return next(); }
104
+ const pairs = txt.split('&');
105
+ // Enforce parameter limit
106
+ if (parameterLimit && pairs.length > parameterLimit)
107
+ {
108
+ return sendError(res, 413, 'too many parameters');
109
+ }
110
+ for (const p of pairs)
111
+ {
112
+ if (!p) continue;
113
+ const eq = p.indexOf('=');
114
+ let k, v;
115
+ if (eq === -1) { k = decodeURIComponent(p.replace(/\+/g, ' ')); v = ''; }
116
+ else { k = decodeURIComponent(p.slice(0, eq).replace(/\+/g, ' ')); v = decodeURIComponent(p.slice(eq + 1).replace(/\+/g, ' ')); }
117
+ // parse key into parts
118
+ const parts = [];
119
+ const re = /([^\[\]]+)|\[(.*?)\]/g;
120
+ let m;
121
+ while ((m = re.exec(k)) !== null)
122
+ {
123
+ const part = m[1] || m[2];
124
+ // Prevent prototype pollution
125
+ if (part === '__proto__' || part === 'constructor' || part === 'prototype') continue;
126
+ parts.push(part);
127
+ }
128
+
129
+ // Enforce depth limit
130
+ if (maxDepth && parts.length > maxDepth)
131
+ {
132
+ return sendError(res, 400, 'nesting depth exceeded');
133
+ }
134
+
135
+ // set value into out following parts
136
+ // _parent/_parentKey track the container holding `cur` so we can
137
+ // convert it from object → array when the `[]` push syntax is used.
138
+ let cur = out;
139
+ let _parent = null;
140
+ let _parentKey = null;
141
+ for (let i = 0; i < parts.length; i++)
142
+ {
143
+ const part = parts[i];
144
+ const isLast = (i === parts.length - 1);
145
+
146
+ if (part === '')
147
+ {
148
+ // Empty-bracket array-push syntax: a[]=val / a[][key]=val
149
+ if (isLast)
150
+ {
151
+ // Ensure cur is an array before pushing, converting parent ref if needed
152
+ if (!Array.isArray(cur))
153
+ {
154
+ const arr = [];
155
+ if (_parent !== null) _parent[_parentKey] = arr;
156
+ cur = arr;
157
+ }
158
+ cur.push(v);
159
+ break;
160
+ }
161
+ // Intermediate empty bracket — navigate into next element of the array
162
+ if (!Array.isArray(cur))
163
+ {
164
+ const arr = [];
165
+ if (_parent !== null) _parent[_parentKey] = arr;
166
+ cur = arr;
167
+ }
168
+ if (cur.length === 0 || typeof cur[cur.length - 1] !== 'object') cur.push({});
169
+ _parent = cur;
170
+ _parentKey = cur.length - 1;
171
+ cur = cur[cur.length - 1];
172
+ continue;
173
+ }
174
+
175
+ // normal key
176
+ if (isLast)
177
+ {
178
+ if (Array.isArray(cur))
179
+ {
180
+ // numeric key may indicate index
181
+ const idx = Number(part);
182
+ if (!Number.isNaN(idx)) cur[idx] = appendValue(cur[idx], v);
183
+ else cur[part] = appendValue(cur[part], v);
184
+ }
185
+ else
186
+ {
187
+ cur[part] = appendValue(cur[part], v);
188
+ }
189
+ }
190
+ else
191
+ {
192
+ if (Array.isArray(cur))
193
+ {
194
+ const idx = Number(part);
195
+ if (!Number.isNaN(idx))
196
+ {
197
+ if (!cur[idx]) cur[idx] = {};
198
+ _parent = cur;
199
+ _parentKey = idx;
200
+ cur = cur[idx];
201
+ } else
202
+ {
203
+ // Non-numeric key on array — navigate into last pushed object
204
+ if (cur.length === 0) cur.push({});
205
+ if (typeof cur[cur.length - 1] !== 'object') cur.push({});
206
+ const obj = cur[cur.length - 1];
207
+ if (!obj[part]) obj[part] = {};
208
+ _parent = obj;
209
+ _parentKey = part;
210
+ cur = obj[part];
211
+ }
212
+ }
213
+ else
214
+ {
215
+ if (!cur[part]) cur[part] = {};
216
+ _parent = cur;
217
+ _parentKey = part;
218
+ cur = cur[part];
219
+ }
220
+ }
221
+ }
222
+ }
223
+ req.body = out;
224
+ }
225
+ } catch (err)
226
+ {
227
+ if (err && err.status === 413) return sendError(res, 413, 'payload too large');
228
+ if (err && err.status === 415) return sendError(res, 415, err.message || 'unsupported encoding');
229
+ req.body = {};
230
+ }
231
+ next();
232
+ };
233
+ }
234
+
235
+ module.exports = urlencoded;