@emeryld/rrroutes-server 2.4.9 → 2.5.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.
package/README.md CHANGED
@@ -68,8 +68,8 @@ const server = createRRRoute(app, {
68
68
  user: await loadUser(req),
69
69
  routesLogger: console,
70
70
  }), // ctx lives on res.locals[CTX_SYMBOL]
71
- globalMiddleware: {
72
- before: [
71
+ middleware: {
72
+ postCtx: [
73
73
  ({ ctx, next }) => {
74
74
  if (!ctx.user) throw new Error('unauthorized')
75
75
  next()
@@ -169,7 +169,7 @@ const server = createRRRoute(app, {
169
169
 
170
170
  ### Middleware order and ctx usage
171
171
 
172
- Order: `resolve` → `ctx` → `global.before` → `route.before` → handler.
172
+ Order: `sanitizer` → `preCtx` → `resolve` → `ctx` → `postCtx` → `route.before` → handler.
173
173
 
174
174
  ```ts
175
175
  import { getCtx, CtxRequestHandler } from '@emeryld/rrroutes-server'
@@ -181,7 +181,7 @@ const audit: CtxRequestHandler<Ctx> = ({ ctx, req, next }) => {
181
181
 
182
182
  const server = createRRRoute(app, {
183
183
  buildCtx: (req, res) => ({ user: res.locals.user, routesLogger: console }),
184
- globalMiddleware: { before: [audit] },
184
+ middleware: { postCtx: [audit] },
185
185
  })
186
186
 
187
187
  const routeBefore = ({ params, query, body, ctx, next }) => {
@@ -199,7 +199,38 @@ app.use((req, res, next) => {
199
199
 
200
200
  - `CtxRequestHandler` receives `{ req, res, next, ctx }` with your typed ctx.
201
201
  - `route.before` handlers now receive the same parsed `params`, `query`, and `body` payload as the handler, alongside `req`, `res`, and `ctx`.
202
- - Need post-response hooks? Register a middleware that wires `res.on('finish', handler)` inside `route.before`/`global.before` instead of relying on a dedicated "after" stage.
202
+ - Need post-response hooks? Register a middleware that wires `res.on('finish', handler)` inside `route.before`/`middleware.postCtx` instead of relying on a dedicated "after" stage.
203
+
204
+ ### Request sanitization
205
+
206
+ Use `middleware.sanitizer` when you want to sanitize raw request data before RRRoutes parses params/query/body.
207
+
208
+ ```ts
209
+ const server = createRRRoute(app, {
210
+ buildCtx,
211
+ middleware: {
212
+ sanitizer: {
213
+ trimStrings: true,
214
+ customSanitizer: (value, context) => {
215
+ if (context.target === 'query' && typeof value === 'string') {
216
+ return value.toLowerCase()
217
+ }
218
+ return value
219
+ },
220
+ },
221
+ },
222
+ })
223
+ ```
224
+
225
+ By default, the sanitizer:
226
+
227
+ - strips null bytes from strings
228
+ - removes prototype-pollution keys (`__proto__`, `prototype`, `constructor`)
229
+ - keeps whitespace unless `trimStrings: true` is set
230
+
231
+ `blockedKeys` exists to prevent prototype-pollution payloads from surviving into downstream object merges.
232
+
233
+ For full sanitizer docs/options, see `./SANITIZER.md`.
203
234
 
204
235
  ### Upload parsing
205
236
 
@@ -272,7 +303,7 @@ Context logger passthrough: if `buildCtx` provides `routesLogger`, handler debug
272
303
 
273
304
  - **Combine registries:** build leaves per domain, spread before `finalize([...usersLeaves, ...projectsLeaves])`, then register once.
274
305
  - **Fail fast on missing controllers:** use `bindAll(...)` for compile-time coverage or call `warnMissingControllers(...)` during startup to surface missing routes.
275
- - **Operator-specific middleware:** attach `route.before` per controller (e.g., role checks) and keep `global.before` minimal (auth/session parsing).
306
+ - **Operator-specific middleware:** attach `route.before` per controller (e.g., role checks) and keep `middleware.postCtx` minimal (auth/session parsing).
276
307
 
277
308
  ## Socket server (typed events, heartbeat, rooms)
278
309
 
package/dist/index.cjs CHANGED
@@ -37,9 +37,11 @@ __export(index_exports, {
37
37
  contractKeyOf: () => import_rrroutes_contract2.keyOf,
38
38
  createConnectionLoggingMiddleware: () => createConnectionLoggingMiddleware,
39
39
  createRRRoute: () => createRRRoute,
40
+ createRequestSanitizationMiddleware: () => createRequestSanitizationMiddleware,
40
41
  createSocketConnections: () => createSocketConnections,
41
42
  defineControllers: () => defineControllers,
42
43
  getCtx: () => getCtx,
44
+ requestSanitizationMiddleware: () => requestSanitizationMiddleware,
43
45
  warnMissingControllers: () => warnMissingControllers
44
46
  });
45
47
  module.exports = __toCommonJS(index_exports);
@@ -48,6 +50,167 @@ var import_rrroutes_contract2 = require("@emeryld/rrroutes-contract");
48
50
  // src/routesV3.server.ts
49
51
  var import_rrroutes_contract = require("@emeryld/rrroutes-contract");
50
52
  var import_multer = __toESM(require("multer"), 1);
53
+
54
+ // src/routesV3.server.sanitize.ts
55
+ var defaultTargets = ["params", "query", "body"];
56
+ var defaultBlockedKeys = ["__proto__", "prototype", "constructor"];
57
+ var defaultMaxDepth = 20;
58
+ var nullBytePattern = /\u0000/g;
59
+ var isPlainObject = (value) => {
60
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
61
+ const proto = Object.getPrototypeOf(value);
62
+ return proto === Object.prototype || proto === null;
63
+ };
64
+ var normalizeOptions = (options) => {
65
+ return {
66
+ targets: new Set(options.targets ?? defaultTargets),
67
+ trimStrings: options.trimStrings ?? false,
68
+ stripNullBytes: options.stripNullBytes ?? true,
69
+ stripPrototypePollutionKeys: options.stripPrototypePollutionKeys ?? true,
70
+ blockedKeys: new Set(options.blockedKeys ?? defaultBlockedKeys),
71
+ maxDepth: options.maxDepth ?? defaultMaxDepth,
72
+ customSanitizer: options.customSanitizer
73
+ };
74
+ };
75
+ var applyCustomSanitizer = (value, options, context) => {
76
+ if (!options.customSanitizer) return value;
77
+ return options.customSanitizer(value, context);
78
+ };
79
+ var sanitizeString = (value, options) => {
80
+ let next = value;
81
+ if (options.stripNullBytes && next.includes("\0")) {
82
+ next = next.replace(nullBytePattern, "");
83
+ }
84
+ if (options.trimStrings) {
85
+ next = next.trim();
86
+ }
87
+ return next;
88
+ };
89
+ var sanitizeValue = (value, options, depth, seen, req, target, path) => {
90
+ const context = {
91
+ req,
92
+ target,
93
+ path,
94
+ depth
95
+ };
96
+ if (depth > options.maxDepth) {
97
+ return applyCustomSanitizer(value, options, context);
98
+ }
99
+ if (typeof value === "string") {
100
+ const sanitized = sanitizeString(value, options);
101
+ return applyCustomSanitizer(sanitized, options, context);
102
+ }
103
+ if (value && typeof value === "object" && seen.has(value)) {
104
+ return applyCustomSanitizer(value, options, context);
105
+ }
106
+ if (Array.isArray(value)) {
107
+ seen.add(value);
108
+ const next = value.map(
109
+ (entry, index) => sanitizeValue(entry, options, depth + 1, seen, req, target, [
110
+ ...path,
111
+ index
112
+ ])
113
+ );
114
+ seen.delete(value);
115
+ return applyCustomSanitizer(next, options, context);
116
+ }
117
+ if (!isPlainObject(value)) {
118
+ return applyCustomSanitizer(value, options, context);
119
+ }
120
+ seen.add(value);
121
+ const source = value;
122
+ const objectTarget = Object.getPrototypeOf(source) === null ? /* @__PURE__ */ Object.create(null) : {};
123
+ for (const [key, entry] of Object.entries(source)) {
124
+ if (options.stripPrototypePollutionKeys && options.blockedKeys.has(key)) {
125
+ continue;
126
+ }
127
+ ;
128
+ objectTarget[key] = sanitizeValue(
129
+ entry,
130
+ options,
131
+ depth + 1,
132
+ seen,
133
+ req,
134
+ context.target,
135
+ [...path, key]
136
+ );
137
+ }
138
+ seen.delete(value);
139
+ return applyCustomSanitizer(objectTarget, options, context);
140
+ };
141
+ var findPropertyDescriptor = (source, key) => {
142
+ let cursor = source;
143
+ while (cursor) {
144
+ const descriptor = Object.getOwnPropertyDescriptor(cursor, key);
145
+ if (descriptor) return descriptor;
146
+ cursor = Object.getPrototypeOf(cursor);
147
+ }
148
+ return void 0;
149
+ };
150
+ var setRequestQuery = (req, value) => {
151
+ const queryDescriptor = findPropertyDescriptor(req, "query");
152
+ if (!queryDescriptor || queryDescriptor.writable || queryDescriptor.set) {
153
+ ;
154
+ req.query = value;
155
+ return;
156
+ }
157
+ Object.defineProperty(req, "query", {
158
+ configurable: true,
159
+ enumerable: true,
160
+ writable: true,
161
+ value
162
+ });
163
+ };
164
+ var createRequestSanitizationMiddleware = (options = {}) => {
165
+ const normalized = normalizeOptions(options);
166
+ return (req, _res, next) => {
167
+ try {
168
+ if (normalized.targets.has("params") && req.params) {
169
+ req.params = sanitizeValue(
170
+ req.params,
171
+ normalized,
172
+ 0,
173
+ /* @__PURE__ */ new WeakSet(),
174
+ req,
175
+ "params",
176
+ []
177
+ );
178
+ }
179
+ if (normalized.targets.has("query")) {
180
+ const query = req.query;
181
+ if (query) {
182
+ const sanitizedQuery = sanitizeValue(
183
+ query,
184
+ normalized,
185
+ 0,
186
+ /* @__PURE__ */ new WeakSet(),
187
+ req,
188
+ "query",
189
+ []
190
+ );
191
+ setRequestQuery(req, sanitizedQuery);
192
+ }
193
+ }
194
+ if (normalized.targets.has("body") && req.body !== void 0) {
195
+ req.body = sanitizeValue(
196
+ req.body,
197
+ normalized,
198
+ 0,
199
+ /* @__PURE__ */ new WeakSet(),
200
+ req,
201
+ "body",
202
+ []
203
+ );
204
+ }
205
+ next();
206
+ } catch (err) {
207
+ next(err);
208
+ }
209
+ };
210
+ };
211
+ var requestSanitizationMiddleware = createRequestSanitizationMiddleware();
212
+
213
+ // src/routesV3.server.ts
51
214
  var serverDebugEventTypes = [
52
215
  "register",
53
216
  "request",
@@ -81,12 +244,12 @@ function createServerDebugEmitter(option) {
81
244
  }
82
245
  return disabled;
83
246
  }
84
- var isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
247
+ var isPlainObject2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
85
248
  var decodeJsonLikeQueryValue = (value) => {
86
249
  if (Array.isArray(value)) {
87
250
  return value.map((entry) => decodeJsonLikeQueryValue(entry));
88
251
  }
89
- if (isPlainObject(value)) {
252
+ if (isPlainObject2(value)) {
90
253
  const next = {};
91
254
  for (const [key, child] of Object.entries(value)) {
92
255
  next[key] = decodeJsonLikeQueryValue(child);
@@ -110,7 +273,7 @@ var REQUEST_PAYLOAD_SYMBOL = /* @__PURE__ */ Symbol.for(
110
273
  "typedLeaves.requestPayload"
111
274
  );
112
275
  function isMulterFile(value) {
113
- if (!isPlainObject(value)) return false;
276
+ if (!isPlainObject2(value)) return false;
114
277
  const candidate = value;
115
278
  return typeof candidate.fieldname === "string";
116
279
  }
@@ -126,7 +289,7 @@ function collectMulterFiles(req) {
126
289
  files.push(value);
127
290
  return;
128
291
  }
129
- if (isPlainObject(value)) {
292
+ if (isPlainObject2(value)) {
130
293
  Object.values(value).forEach(pushValue);
131
294
  }
132
295
  };
@@ -252,9 +415,12 @@ function createRRRoute(router, config) {
252
415
  if (!isVerbose || !details) return event;
253
416
  return { ...event, ...details };
254
417
  };
255
- const globalBeforeMws = [...config.globalMiddleware ?? []].map(
418
+ const middlewareConfig = config.middleware ?? {};
419
+ const postCtxMws = [...middlewareConfig.postCtx ?? []].map(
256
420
  (mw) => adaptCtxMw(mw)
257
421
  );
422
+ const preCtxMws = [...middlewareConfig.preCtx ?? []];
423
+ const sanitizerMw = middlewareConfig.sanitizer === void 0 ? void 0 : typeof middlewareConfig.sanitizer === "function" ? middlewareConfig.sanitizer : createRequestSanitizationMiddleware(middlewareConfig.sanitizer);
258
424
  const registered = getRegisteredRouteStore(router);
259
425
  const getMulterOptions = (fields) => {
260
426
  if (!fields || fields.length === 0) return void 0;
@@ -416,9 +582,11 @@ function createRRRoute(router, config) {
416
582
  }
417
583
  };
418
584
  const before = [
585
+ ...sanitizerMw ? [sanitizerMw] : [],
586
+ ...preCtxMws,
419
587
  resolvePayloadMw,
420
588
  ctxMw,
421
- ...globalBeforeMws,
589
+ ...postCtxMws,
422
590
  ...routeSpecific
423
591
  ];
424
592
  const wrapped = async (req, res, next) => {
@@ -1434,9 +1602,11 @@ var createConnectionLoggingMiddleware = (options = {}) => {
1434
1602
  contractKeyOf,
1435
1603
  createConnectionLoggingMiddleware,
1436
1604
  createRRRoute,
1605
+ createRequestSanitizationMiddleware,
1437
1606
  createSocketConnections,
1438
1607
  defineControllers,
1439
1608
  getCtx,
1609
+ requestSanitizationMiddleware,
1440
1610
  warnMissingControllers
1441
1611
  });
1442
1612
  //# sourceMappingURL=index.cjs.map