@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 +37 -6
- package/dist/index.cjs +176 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +174 -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,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
|
|
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 (
|
|
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 (!
|
|
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 (
|
|
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
|
|
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
|
-
...
|
|
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
|