@emeryld/rrroutes-server 2.4.9 → 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 +37 -6
- package/dist/index.cjs +149 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +147 -6
- package/dist/index.js.map +1 -1
- package/dist/routesV3.server.d.ts +29 -8
- package/dist/routesV3.server.sanitize.d.ts +71 -0
- package/package.json +2 -2
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
|
-
|
|
72
|
-
|
|
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` → `
|
|
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
|
-
|
|
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`/`
|
|
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 `
|
|
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
|
|
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 (
|
|
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 (!
|
|
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 (
|
|
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
|
|
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
|
-
...
|
|
562
|
+
...postCtxMws,
|
|
422
563
|
...routeSpecific
|
|
423
564
|
];
|
|
424
565
|
const wrapped = async (req, res, next) => {
|
|
@@ -1434,9 +1575,11 @@ var createConnectionLoggingMiddleware = (options = {}) => {
|
|
|
1434
1575
|
contractKeyOf,
|
|
1435
1576
|
createConnectionLoggingMiddleware,
|
|
1436
1577
|
createRRRoute,
|
|
1578
|
+
createRequestSanitizationMiddleware,
|
|
1437
1579
|
createSocketConnections,
|
|
1438
1580
|
defineControllers,
|
|
1439
1581
|
getCtx,
|
|
1582
|
+
requestSanitizationMiddleware,
|
|
1440
1583
|
warnMissingControllers
|
|
1441
1584
|
});
|
|
1442
1585
|
//# sourceMappingURL=index.cjs.map
|