@emeryld/rrroutes-server 2.4.8 → 2.5.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.
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,140 @@ 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 createRequestSanitizationMiddleware = (options = {}) => {
142
+ const normalized = normalizeOptions(options);
143
+ return (req, _res, next) => {
144
+ try {
145
+ if (normalized.targets.has("params") && req.params) {
146
+ req.params = sanitizeValue(
147
+ req.params,
148
+ normalized,
149
+ 0,
150
+ /* @__PURE__ */ new WeakSet(),
151
+ req,
152
+ "params",
153
+ []
154
+ );
155
+ }
156
+ if (normalized.targets.has("query") && req.query) {
157
+ req.query = sanitizeValue(
158
+ req.query,
159
+ normalized,
160
+ 0,
161
+ /* @__PURE__ */ new WeakSet(),
162
+ req,
163
+ "query",
164
+ []
165
+ );
166
+ }
167
+ if (normalized.targets.has("body") && req.body !== void 0) {
168
+ req.body = sanitizeValue(
169
+ req.body,
170
+ normalized,
171
+ 0,
172
+ /* @__PURE__ */ new WeakSet(),
173
+ req,
174
+ "body",
175
+ []
176
+ );
177
+ }
178
+ next();
179
+ } catch (err) {
180
+ next(err);
181
+ }
182
+ };
183
+ };
184
+ var requestSanitizationMiddleware = createRequestSanitizationMiddleware();
185
+
186
+ // src/routesV3.server.ts
51
187
  var serverDebugEventTypes = [
52
188
  "register",
53
189
  "request",
@@ -81,12 +217,12 @@ function createServerDebugEmitter(option) {
81
217
  }
82
218
  return disabled;
83
219
  }
84
- var isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
220
+ var isPlainObject2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
85
221
  var decodeJsonLikeQueryValue = (value) => {
86
222
  if (Array.isArray(value)) {
87
223
  return value.map((entry) => decodeJsonLikeQueryValue(entry));
88
224
  }
89
- if (isPlainObject(value)) {
225
+ if (isPlainObject2(value)) {
90
226
  const next = {};
91
227
  for (const [key, child] of Object.entries(value)) {
92
228
  next[key] = decodeJsonLikeQueryValue(child);
@@ -110,7 +246,7 @@ var REQUEST_PAYLOAD_SYMBOL = /* @__PURE__ */ Symbol.for(
110
246
  "typedLeaves.requestPayload"
111
247
  );
112
248
  function isMulterFile(value) {
113
- if (!isPlainObject(value)) return false;
249
+ if (!isPlainObject2(value)) return false;
114
250
  const candidate = value;
115
251
  return typeof candidate.fieldname === "string";
116
252
  }
@@ -126,7 +262,7 @@ function collectMulterFiles(req) {
126
262
  files.push(value);
127
263
  return;
128
264
  }
129
- if (isPlainObject(value)) {
265
+ if (isPlainObject2(value)) {
130
266
  Object.values(value).forEach(pushValue);
131
267
  }
132
268
  };
@@ -252,9 +388,12 @@ function createRRRoute(router, config) {
252
388
  if (!isVerbose || !details) return event;
253
389
  return { ...event, ...details };
254
390
  };
255
- const globalBeforeMws = [...config.globalMiddleware ?? []].map(
391
+ const middlewareConfig = config.middleware ?? {};
392
+ const postCtxMws = [...middlewareConfig.postCtx ?? []].map(
256
393
  (mw) => adaptCtxMw(mw)
257
394
  );
395
+ const preCtxMws = [...middlewareConfig.preCtx ?? []];
396
+ const sanitizerMw = middlewareConfig.sanitizer === void 0 ? void 0 : typeof middlewareConfig.sanitizer === "function" ? middlewareConfig.sanitizer : createRequestSanitizationMiddleware(middlewareConfig.sanitizer);
258
397
  const registered = getRegisteredRouteStore(router);
259
398
  const getMulterOptions = (fields) => {
260
399
  if (!fields || fields.length === 0) return void 0;
@@ -416,9 +555,11 @@ function createRRRoute(router, config) {
416
555
  }
417
556
  };
418
557
  const before = [
558
+ ...sanitizerMw ? [sanitizerMw] : [],
559
+ ...preCtxMws,
419
560
  resolvePayloadMw,
420
561
  ctxMw,
421
- ...globalBeforeMws,
562
+ ...postCtxMws,
422
563
  ...routeSpecific
423
564
  ];
424
565
  const wrapped = async (req, res, next) => {
@@ -1120,7 +1261,10 @@ function createSocketConnections(io, events, opts) {
1120
1261
  });
1121
1262
  io.on("connection", builtInConnectionListener);
1122
1263
  const conn = {
1123
- on(eventName, handler) {
1264
+ on({
1265
+ eventName,
1266
+ handler
1267
+ }) {
1124
1268
  const socketListeners = /* @__PURE__ */ new WeakMap();
1125
1269
  const connectionListener = (socket) => {
1126
1270
  const wrapped = async (raw) => {
@@ -1229,7 +1373,12 @@ function createSocketConnections(io, events, opts) {
1229
1373
  off(eventName) {
1230
1374
  removeAllForEvent(String(eventName));
1231
1375
  },
1232
- emitAny(eventName, payload, rooms, metadata) {
1376
+ emitAny({
1377
+ eventName,
1378
+ payload,
1379
+ rooms,
1380
+ metadata
1381
+ }) {
1233
1382
  const targets = toArray2(rooms);
1234
1383
  if (Object.prototype.hasOwnProperty.call(events, eventName)) {
1235
1384
  emitToTargets(
@@ -1426,9 +1575,11 @@ var createConnectionLoggingMiddleware = (options = {}) => {
1426
1575
  contractKeyOf,
1427
1576
  createConnectionLoggingMiddleware,
1428
1577
  createRRRoute,
1578
+ createRequestSanitizationMiddleware,
1429
1579
  createSocketConnections,
1430
1580
  defineControllers,
1431
1581
  getCtx,
1582
+ requestSanitizationMiddleware,
1432
1583
  warnMissingControllers
1433
1584
  });
1434
1585
  //# sourceMappingURL=index.cjs.map